mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 02:12:07 +00:00
refactor: split claude cli history import pipeline
This commit is contained in:
35
src/chat/tool-content.ts
Normal file
35
src/chat/tool-content.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type ToolContentBlock = Record<string, unknown>;
|
||||
|
||||
export function normalizeToolContentType(value: unknown): string {
|
||||
return typeof value === "string" ? value.toLowerCase() : "";
|
||||
}
|
||||
|
||||
export function isToolCallContentType(value: unknown): boolean {
|
||||
const type = normalizeToolContentType(value);
|
||||
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use";
|
||||
}
|
||||
|
||||
export function isToolResultContentType(value: unknown): boolean {
|
||||
const type = normalizeToolContentType(value);
|
||||
return type === "toolresult" || type === "tool_result";
|
||||
}
|
||||
|
||||
export function isToolCallBlock(block: ToolContentBlock): boolean {
|
||||
return isToolCallContentType(block.type);
|
||||
}
|
||||
|
||||
export function isToolResultBlock(block: ToolContentBlock): boolean {
|
||||
return isToolResultContentType(block.type);
|
||||
}
|
||||
|
||||
export function resolveToolBlockArgs(block: ToolContentBlock): unknown {
|
||||
return block.args ?? block.arguments ?? block.input;
|
||||
}
|
||||
|
||||
export function resolveToolUseId(block: ToolContentBlock): string | undefined {
|
||||
const id =
|
||||
(typeof block.id === "string" && block.id.trim()) ||
|
||||
(typeof block.tool_use_id === "string" && block.tool_use_id.trim()) ||
|
||||
(typeof block.toolUseId === "string" && block.toolUseId.trim());
|
||||
return id || undefined;
|
||||
}
|
||||
334
src/gateway/cli-session-history.claude.ts
Normal file
334
src/gateway/cli-session-history.claude.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
isToolCallBlock,
|
||||
isToolResultBlock,
|
||||
resolveToolUseId,
|
||||
type ToolContentBlock,
|
||||
} from "../chat/tool-content.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { attachOpenClawTranscriptMeta } from "./session-utils.fs.js";
|
||||
|
||||
export const CLAUDE_CLI_PROVIDER = "claude-cli";
|
||||
const CLAUDE_PROJECTS_RELATIVE_DIR = path.join(".claude", "projects");
|
||||
|
||||
type ClaudeCliProjectEntry = {
|
||||
type?: unknown;
|
||||
timestamp?: unknown;
|
||||
uuid?: unknown;
|
||||
isSidechain?: unknown;
|
||||
message?: {
|
||||
role?: unknown;
|
||||
content?: unknown;
|
||||
model?: unknown;
|
||||
stop_reason?: unknown;
|
||||
usage?: {
|
||||
input_tokens?: unknown;
|
||||
output_tokens?: unknown;
|
||||
cache_read_input_tokens?: unknown;
|
||||
cache_creation_input_tokens?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type ClaudeCliMessage = NonNullable<ClaudeCliProjectEntry["message"]>;
|
||||
type ClaudeCliUsage = ClaudeCliMessage["usage"];
|
||||
type TranscriptLikeMessage = Record<string, unknown>;
|
||||
type ToolNameRegistry = Map<string, string>;
|
||||
|
||||
function resolveHistoryHomeDir(homeDir?: string): string {
|
||||
return homeDir?.trim() || process.env.HOME || os.homedir();
|
||||
}
|
||||
|
||||
function resolveClaudeProjectsDir(homeDir?: string): string {
|
||||
return path.join(resolveHistoryHomeDir(homeDir), CLAUDE_PROJECTS_RELATIVE_DIR);
|
||||
}
|
||||
|
||||
export function resolveClaudeCliBindingSessionId(
|
||||
entry: SessionEntry | undefined,
|
||||
): string | undefined {
|
||||
const bindingSessionId = entry?.cliSessionBindings?.[CLAUDE_CLI_PROVIDER]?.sessionId?.trim();
|
||||
if (bindingSessionId) {
|
||||
return bindingSessionId;
|
||||
}
|
||||
const legacyMapSessionId = entry?.cliSessionIds?.[CLAUDE_CLI_PROVIDER]?.trim();
|
||||
if (legacyMapSessionId) {
|
||||
return legacyMapSessionId;
|
||||
}
|
||||
const legacyClaudeSessionId = entry?.claudeCliSessionId?.trim();
|
||||
return legacyClaudeSessionId || undefined;
|
||||
}
|
||||
|
||||
function resolveFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveTimestampMs(value: unknown): number | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function resolveClaudeCliUsage(raw: ClaudeCliUsage) {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const input = resolveFiniteNumber(raw.input_tokens);
|
||||
const output = resolveFiniteNumber(raw.output_tokens);
|
||||
const cacheRead = resolveFiniteNumber(raw.cache_read_input_tokens);
|
||||
const cacheWrite = resolveFiniteNumber(raw.cache_creation_input_tokens);
|
||||
if (
|
||||
input === undefined &&
|
||||
output === undefined &&
|
||||
cacheRead === undefined &&
|
||||
cacheWrite === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(input !== undefined ? { input } : {}),
|
||||
...(output !== undefined ? { output } : {}),
|
||||
...(cacheRead !== undefined ? { cacheRead } : {}),
|
||||
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneJsonValue<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function normalizeClaudeCliContent(
|
||||
content: string | unknown[],
|
||||
toolNameRegistry: ToolNameRegistry,
|
||||
): string | unknown[] {
|
||||
if (!Array.isArray(content)) {
|
||||
return cloneJsonValue(content);
|
||||
}
|
||||
|
||||
const normalized: ToolContentBlock[] = [];
|
||||
for (const item of content) {
|
||||
if (!item || typeof item !== "object") {
|
||||
normalized.push(cloneJsonValue(item as ToolContentBlock));
|
||||
continue;
|
||||
}
|
||||
const block = cloneJsonValue(item as ToolContentBlock);
|
||||
const type = typeof block.type === "string" ? block.type : "";
|
||||
if (type === "tool_use") {
|
||||
const id = typeof block.id === "string" ? block.id.trim() : "";
|
||||
const name = typeof block.name === "string" ? block.name.trim() : "";
|
||||
if (id && name) {
|
||||
toolNameRegistry.set(id, name);
|
||||
}
|
||||
if (block.input !== undefined && block.arguments === undefined) {
|
||||
block.arguments = cloneJsonValue(block.input);
|
||||
}
|
||||
block.type = "toolcall";
|
||||
delete block.input;
|
||||
normalized.push(block);
|
||||
continue;
|
||||
}
|
||||
if (type === "tool_result") {
|
||||
const toolUseId = resolveToolUseId(block);
|
||||
if (!block.name && toolUseId) {
|
||||
const toolName = toolNameRegistry.get(toolUseId);
|
||||
if (toolName) {
|
||||
block.name = toolName;
|
||||
}
|
||||
}
|
||||
normalized.push(block);
|
||||
continue;
|
||||
}
|
||||
normalized.push(block);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getMessageBlocks(message: unknown): ToolContentBlock[] | null {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
return Array.isArray(content) ? (content as ToolContentBlock[]) : null;
|
||||
}
|
||||
|
||||
function isAssistantToolCallMessage(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
if (role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const blocks = getMessageBlocks(message);
|
||||
return Boolean(blocks && blocks.length > 0 && blocks.every(isToolCallBlock));
|
||||
}
|
||||
|
||||
function isUserToolResultMessage(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
if (role !== "user") {
|
||||
return false;
|
||||
}
|
||||
const blocks = getMessageBlocks(message);
|
||||
return Boolean(blocks && blocks.length > 0 && blocks.every(isToolResultBlock));
|
||||
}
|
||||
|
||||
function coalesceClaudeCliToolMessages(messages: TranscriptLikeMessage[]): TranscriptLikeMessage[] {
|
||||
const coalesced: TranscriptLikeMessage[] = [];
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const current = messages[index];
|
||||
const next = messages[index + 1];
|
||||
if (!isAssistantToolCallMessage(current) || !isUserToolResultMessage(next)) {
|
||||
coalesced.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
const callBlocks = getMessageBlocks(current) ?? [];
|
||||
const resultBlocks = getMessageBlocks(next) ?? [];
|
||||
const callIds = new Set(
|
||||
callBlocks.map(resolveToolUseId).filter((id): id is string => Boolean(id)),
|
||||
);
|
||||
const allResultsMatch =
|
||||
resultBlocks.length > 0 &&
|
||||
resultBlocks.every((block) => {
|
||||
const toolUseId = resolveToolUseId(block);
|
||||
return Boolean(toolUseId && callIds.has(toolUseId));
|
||||
});
|
||||
if (!allResultsMatch) {
|
||||
coalesced.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
coalesced.push({
|
||||
...current,
|
||||
content: [...callBlocks.map(cloneJsonValue), ...resultBlocks.map(cloneJsonValue)],
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
return coalesced;
|
||||
}
|
||||
|
||||
function parseClaudeCliHistoryEntry(
|
||||
entry: ClaudeCliProjectEntry,
|
||||
cliSessionId: string,
|
||||
toolNameRegistry: ToolNameRegistry,
|
||||
): TranscriptLikeMessage | null {
|
||||
if (entry.isSidechain === true || !entry.message || typeof entry.message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const type = typeof entry.type === "string" ? entry.type : undefined;
|
||||
const role = typeof entry.message.role === "string" ? entry.message.role : undefined;
|
||||
if ((type !== "user" && type !== "assistant") || role !== type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = resolveTimestampMs(entry.timestamp);
|
||||
const baseMeta = {
|
||||
importedFrom: CLAUDE_CLI_PROVIDER,
|
||||
cliSessionId,
|
||||
...(typeof entry.uuid === "string" && entry.uuid.trim() ? { externalId: entry.uuid } : {}),
|
||||
};
|
||||
|
||||
const content =
|
||||
typeof entry.message.content === "string" || Array.isArray(entry.message.content)
|
||||
? normalizeClaudeCliContent(entry.message.content, toolNameRegistry)
|
||||
: undefined;
|
||||
if (content === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === "user") {
|
||||
return attachOpenClawTranscriptMeta(
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
...(timestamp !== undefined ? { timestamp } : {}),
|
||||
},
|
||||
baseMeta,
|
||||
) as TranscriptLikeMessage;
|
||||
}
|
||||
|
||||
return attachOpenClawTranscriptMeta(
|
||||
{
|
||||
role: "assistant",
|
||||
content,
|
||||
api: "anthropic-messages",
|
||||
provider: CLAUDE_CLI_PROVIDER,
|
||||
...(typeof entry.message.model === "string" && entry.message.model.trim()
|
||||
? { model: entry.message.model }
|
||||
: {}),
|
||||
...(typeof entry.message.stop_reason === "string" && entry.message.stop_reason.trim()
|
||||
? { stopReason: entry.message.stop_reason }
|
||||
: {}),
|
||||
...(resolveClaudeCliUsage(entry.message.usage)
|
||||
? { usage: resolveClaudeCliUsage(entry.message.usage) }
|
||||
: {}),
|
||||
...(timestamp !== undefined ? { timestamp } : {}),
|
||||
},
|
||||
baseMeta,
|
||||
) as TranscriptLikeMessage;
|
||||
}
|
||||
|
||||
export function resolveClaudeCliSessionFilePath(params: {
|
||||
cliSessionId: string;
|
||||
homeDir?: string;
|
||||
}): string | undefined {
|
||||
const projectsDir = resolveClaudeProjectsDir(params.homeDir);
|
||||
let projectEntries: fs.Dirent[];
|
||||
try {
|
||||
projectEntries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const entry of projectEntries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const candidate = path.join(projectsDir, entry.name, `${params.cliSessionId}.jsonl`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function readClaudeCliSessionMessages(params: {
|
||||
cliSessionId: string;
|
||||
homeDir?: string;
|
||||
}): TranscriptLikeMessage[] {
|
||||
const filePath = resolveClaudeCliSessionFilePath(params);
|
||||
if (!filePath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages: TranscriptLikeMessage[] = [];
|
||||
const toolNameRegistry: ToolNameRegistry = new Map();
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(line) as ClaudeCliProjectEntry;
|
||||
const message = parseClaudeCliHistoryEntry(parsed, params.cliSessionId, toolNameRegistry);
|
||||
if (message) {
|
||||
messages.push(message);
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed external history entries.
|
||||
}
|
||||
}
|
||||
return coalesceClaudeCliToolMessages(messages);
|
||||
}
|
||||
132
src/gateway/cli-session-history.merge.ts
Normal file
132
src/gateway/cli-session-history.merge.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js";
|
||||
|
||||
const DEDUPE_TIMESTAMP_WINDOW_MS = 5 * 60 * 1000;
|
||||
|
||||
function extractComparableText(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const record = message as { role?: unknown; text?: unknown; content?: unknown };
|
||||
const role = typeof record.role === "string" ? record.role : undefined;
|
||||
const parts: string[] = [];
|
||||
if (typeof record.text === "string") {
|
||||
parts.push(record.text);
|
||||
}
|
||||
if (typeof record.content === "string") {
|
||||
parts.push(record.content);
|
||||
} else if (Array.isArray(record.content)) {
|
||||
for (const block of record.content) {
|
||||
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
||||
parts.push(block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const joined = parts.join("\n").trim();
|
||||
if (!joined) {
|
||||
return undefined;
|
||||
}
|
||||
const visible = role === "user" ? stripInboundMetadata(joined) : joined;
|
||||
const normalized = visible.replace(/\s+/g, " ").trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function resolveFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveComparableTimestamp(message: unknown): number | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return resolveFiniteNumber((message as { timestamp?: unknown }).timestamp);
|
||||
}
|
||||
|
||||
function resolveComparableRole(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
return typeof role === "string" ? role : undefined;
|
||||
}
|
||||
|
||||
function resolveImportedExternalId(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const meta =
|
||||
"__openclaw" in message &&
|
||||
(message as { __openclaw?: unknown }).__openclaw &&
|
||||
typeof (message as { __openclaw?: unknown }).__openclaw === "object"
|
||||
? ((message as { __openclaw?: Record<string, unknown> }).__openclaw ?? {})
|
||||
: undefined;
|
||||
const externalId = meta?.externalId;
|
||||
return typeof externalId === "string" && externalId.trim() ? externalId : undefined;
|
||||
}
|
||||
|
||||
function isEquivalentImportedMessage(existing: unknown, imported: unknown): boolean {
|
||||
const importedExternalId = resolveImportedExternalId(imported);
|
||||
if (importedExternalId && resolveImportedExternalId(existing) === importedExternalId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const existingRole = resolveComparableRole(existing);
|
||||
const importedRole = resolveComparableRole(imported);
|
||||
if (!existingRole || existingRole !== importedRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingText = extractComparableText(existing);
|
||||
const importedText = extractComparableText(imported);
|
||||
if (!existingText || !importedText || existingText !== importedText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingTimestamp = resolveComparableTimestamp(existing);
|
||||
const importedTimestamp = resolveComparableTimestamp(imported);
|
||||
if (existingTimestamp === undefined || importedTimestamp === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Math.abs(existingTimestamp - importedTimestamp) <= DEDUPE_TIMESTAMP_WINDOW_MS;
|
||||
}
|
||||
|
||||
function compareHistoryMessages(
|
||||
a: { message: unknown; order: number },
|
||||
b: { message: unknown; order: number },
|
||||
): number {
|
||||
const aTimestamp = resolveComparableTimestamp(a.message);
|
||||
const bTimestamp = resolveComparableTimestamp(b.message);
|
||||
if (aTimestamp !== undefined && bTimestamp !== undefined && aTimestamp !== bTimestamp) {
|
||||
return aTimestamp - bTimestamp;
|
||||
}
|
||||
if (aTimestamp !== undefined && bTimestamp === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (aTimestamp === undefined && bTimestamp !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
return a.order - b.order;
|
||||
}
|
||||
|
||||
export function mergeImportedChatHistoryMessages(params: {
|
||||
localMessages: unknown[];
|
||||
importedMessages: unknown[];
|
||||
}): unknown[] {
|
||||
if (params.importedMessages.length === 0) {
|
||||
return params.localMessages;
|
||||
}
|
||||
const merged = params.localMessages.map((message, index) => ({ message, order: index }));
|
||||
let nextOrder = merged.length;
|
||||
for (const imported of params.importedMessages) {
|
||||
if (merged.some((existing) => isEquivalentImportedMessage(existing.message, imported))) {
|
||||
continue;
|
||||
}
|
||||
merged.push({ message: imported, order: nextOrder });
|
||||
nextOrder += 1;
|
||||
}
|
||||
merged.sort(compareHistoryMessages);
|
||||
return merged.map((entry) => entry.message);
|
||||
}
|
||||
@@ -1,486 +1,19 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { attachOpenClawTranscriptMeta } from "./session-utils.fs.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROVIDER,
|
||||
readClaudeCliSessionMessages,
|
||||
resolveClaudeCliBindingSessionId,
|
||||
resolveClaudeCliSessionFilePath,
|
||||
} from "./cli-session-history.claude.js";
|
||||
import { mergeImportedChatHistoryMessages } from "./cli-session-history.merge.js";
|
||||
|
||||
const CLAUDE_CLI_PROVIDER = "claude-cli";
|
||||
const CLAUDE_PROJECTS_RELATIVE_DIR = path.join(".claude", "projects");
|
||||
const DEDUPE_TIMESTAMP_WINDOW_MS = 5 * 60 * 1000;
|
||||
|
||||
type ClaudeCliProjectEntry = {
|
||||
type?: unknown;
|
||||
timestamp?: unknown;
|
||||
uuid?: unknown;
|
||||
isSidechain?: unknown;
|
||||
message?: {
|
||||
role?: unknown;
|
||||
content?: unknown;
|
||||
model?: unknown;
|
||||
stop_reason?: unknown;
|
||||
usage?: {
|
||||
input_tokens?: unknown;
|
||||
output_tokens?: unknown;
|
||||
cache_read_input_tokens?: unknown;
|
||||
cache_creation_input_tokens?: unknown;
|
||||
};
|
||||
};
|
||||
export {
|
||||
mergeImportedChatHistoryMessages,
|
||||
readClaudeCliSessionMessages,
|
||||
resolveClaudeCliSessionFilePath,
|
||||
};
|
||||
|
||||
type ClaudeCliMessage = NonNullable<ClaudeCliProjectEntry["message"]>;
|
||||
type ClaudeCliUsage = ClaudeCliMessage["usage"];
|
||||
|
||||
type TranscriptLikeMessage = Record<string, unknown>;
|
||||
type ToolNameRegistry = Map<string, string>;
|
||||
|
||||
function resolveHistoryHomeDir(homeDir?: string): string {
|
||||
return homeDir?.trim() || process.env.HOME || os.homedir();
|
||||
}
|
||||
|
||||
function resolveClaudeProjectsDir(homeDir?: string): string {
|
||||
return path.join(resolveHistoryHomeDir(homeDir), CLAUDE_PROJECTS_RELATIVE_DIR);
|
||||
}
|
||||
|
||||
function resolveClaudeCliBindingSessionId(entry: SessionEntry | undefined): string | undefined {
|
||||
const bindingSessionId = entry?.cliSessionBindings?.[CLAUDE_CLI_PROVIDER]?.sessionId?.trim();
|
||||
if (bindingSessionId) {
|
||||
return bindingSessionId;
|
||||
}
|
||||
const legacyMapSessionId = entry?.cliSessionIds?.[CLAUDE_CLI_PROVIDER]?.trim();
|
||||
if (legacyMapSessionId) {
|
||||
return legacyMapSessionId;
|
||||
}
|
||||
const legacyClaudeSessionId = entry?.claudeCliSessionId?.trim();
|
||||
return legacyClaudeSessionId || undefined;
|
||||
}
|
||||
|
||||
function resolveFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveTimestampMs(value: unknown): number | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function resolveClaudeCliUsage(raw: ClaudeCliUsage) {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const input = resolveFiniteNumber(raw.input_tokens);
|
||||
const output = resolveFiniteNumber(raw.output_tokens);
|
||||
const cacheRead = resolveFiniteNumber(raw.cache_read_input_tokens);
|
||||
const cacheWrite = resolveFiniteNumber(raw.cache_creation_input_tokens);
|
||||
if (
|
||||
input === undefined &&
|
||||
output === undefined &&
|
||||
cacheRead === undefined &&
|
||||
cacheWrite === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(input !== undefined ? { input } : {}),
|
||||
...(output !== undefined ? { output } : {}),
|
||||
...(cacheRead !== undefined ? { cacheRead } : {}),
|
||||
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneJsonValue<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function normalizeClaudeCliContent(
|
||||
content: string | unknown[],
|
||||
toolNameRegistry: ToolNameRegistry,
|
||||
): string | unknown[] {
|
||||
if (!Array.isArray(content)) {
|
||||
return cloneJsonValue(content);
|
||||
}
|
||||
|
||||
const normalized: Array<Record<string, unknown>> = [];
|
||||
for (const item of content) {
|
||||
if (!item || typeof item !== "object") {
|
||||
normalized.push(cloneJsonValue(item as Record<string, unknown>));
|
||||
continue;
|
||||
}
|
||||
const block = cloneJsonValue(item as Record<string, unknown>);
|
||||
const type = typeof block.type === "string" ? block.type : "";
|
||||
if (type === "tool_use") {
|
||||
const id = typeof block.id === "string" ? block.id.trim() : "";
|
||||
const name = typeof block.name === "string" ? block.name.trim() : "";
|
||||
if (id && name) {
|
||||
toolNameRegistry.set(id, name);
|
||||
}
|
||||
if (block.input !== undefined && block.arguments === undefined) {
|
||||
block.arguments = cloneJsonValue(block.input);
|
||||
}
|
||||
block.type = "toolcall";
|
||||
delete block.input;
|
||||
normalized.push(block);
|
||||
continue;
|
||||
}
|
||||
if (type === "tool_result") {
|
||||
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id.trim() : "";
|
||||
if (!block.name && toolUseId) {
|
||||
const toolName = toolNameRegistry.get(toolUseId);
|
||||
if (toolName) {
|
||||
block.name = toolName;
|
||||
}
|
||||
}
|
||||
normalized.push(block);
|
||||
continue;
|
||||
}
|
||||
normalized.push(block);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getMessageBlocks(message: unknown): Array<Record<string, unknown>> | null {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
return Array.isArray(content) ? (content as Array<Record<string, unknown>>) : null;
|
||||
}
|
||||
|
||||
function isToolCallBlock(block: Record<string, unknown>): boolean {
|
||||
const type = typeof block.type === "string" ? block.type.toLowerCase() : "";
|
||||
return type === "toolcall" || type === "tool_call" || type === "tooluse" || type === "tool_use";
|
||||
}
|
||||
|
||||
function isToolResultBlock(block: Record<string, unknown>): boolean {
|
||||
const type = typeof block.type === "string" ? block.type.toLowerCase() : "";
|
||||
return type === "toolresult" || type === "tool_result";
|
||||
}
|
||||
|
||||
function resolveToolUseId(block: Record<string, unknown>): string | undefined {
|
||||
const id =
|
||||
(typeof block.id === "string" && block.id.trim()) ||
|
||||
(typeof block.tool_use_id === "string" && block.tool_use_id.trim()) ||
|
||||
(typeof block.toolUseId === "string" && block.toolUseId.trim());
|
||||
return id || undefined;
|
||||
}
|
||||
|
||||
function isAssistantToolCallMessage(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
if (role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const blocks = getMessageBlocks(message);
|
||||
return Boolean(blocks && blocks.length > 0 && blocks.every(isToolCallBlock));
|
||||
}
|
||||
|
||||
function isUserToolResultMessage(message: unknown): boolean {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
if (role !== "user") {
|
||||
return false;
|
||||
}
|
||||
const blocks = getMessageBlocks(message);
|
||||
return Boolean(blocks && blocks.length > 0 && blocks.every(isToolResultBlock));
|
||||
}
|
||||
|
||||
function coalesceClaudeCliToolMessages(messages: TranscriptLikeMessage[]): TranscriptLikeMessage[] {
|
||||
const coalesced: TranscriptLikeMessage[] = [];
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const current = messages[index];
|
||||
const next = messages[index + 1];
|
||||
if (!isAssistantToolCallMessage(current) || !isUserToolResultMessage(next)) {
|
||||
coalesced.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
const callBlocks = getMessageBlocks(current) ?? [];
|
||||
const resultBlocks = getMessageBlocks(next) ?? [];
|
||||
const callIds = new Set(
|
||||
callBlocks.map(resolveToolUseId).filter((id): id is string => Boolean(id)),
|
||||
);
|
||||
const allResultsMatch =
|
||||
resultBlocks.length > 0 &&
|
||||
resultBlocks.every((block) => {
|
||||
const toolUseId = resolveToolUseId(block);
|
||||
return Boolean(toolUseId && callIds.has(toolUseId));
|
||||
});
|
||||
if (!allResultsMatch) {
|
||||
coalesced.push(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
coalesced.push({
|
||||
...current,
|
||||
content: [...callBlocks.map(cloneJsonValue), ...resultBlocks.map(cloneJsonValue)],
|
||||
});
|
||||
index += 1;
|
||||
}
|
||||
return coalesced;
|
||||
}
|
||||
|
||||
function extractComparableText(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const record = message as { role?: unknown; text?: unknown; content?: unknown };
|
||||
const role = typeof record.role === "string" ? record.role : undefined;
|
||||
const parts: string[] = [];
|
||||
if (typeof record.text === "string") {
|
||||
parts.push(record.text);
|
||||
}
|
||||
if (typeof record.content === "string") {
|
||||
parts.push(record.content);
|
||||
} else if (Array.isArray(record.content)) {
|
||||
for (const block of record.content) {
|
||||
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
||||
parts.push(block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const joined = parts.join("\n").trim();
|
||||
if (!joined) {
|
||||
return undefined;
|
||||
}
|
||||
const visible = role === "user" ? stripInboundMetadata(joined) : joined;
|
||||
const normalized = visible.replace(/\s+/g, " ").trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function resolveComparableTimestamp(message: unknown): number | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return resolveFiniteNumber((message as { timestamp?: unknown }).timestamp);
|
||||
}
|
||||
|
||||
function resolveComparableRole(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const role = (message as { role?: unknown }).role;
|
||||
return typeof role === "string" ? role : undefined;
|
||||
}
|
||||
|
||||
function resolveImportedExternalId(message: unknown): string | undefined {
|
||||
if (!message || typeof message !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const meta =
|
||||
"__openclaw" in message &&
|
||||
(message as { __openclaw?: unknown }).__openclaw &&
|
||||
typeof (message as { __openclaw?: unknown }).__openclaw === "object"
|
||||
? ((message as { __openclaw?: Record<string, unknown> }).__openclaw ?? {})
|
||||
: undefined;
|
||||
const externalId = meta?.externalId;
|
||||
return typeof externalId === "string" && externalId.trim() ? externalId : undefined;
|
||||
}
|
||||
|
||||
function isEquivalentImportedMessage(existing: unknown, imported: unknown): boolean {
|
||||
const importedExternalId = resolveImportedExternalId(imported);
|
||||
if (importedExternalId && resolveImportedExternalId(existing) === importedExternalId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const existingRole = resolveComparableRole(existing);
|
||||
const importedRole = resolveComparableRole(imported);
|
||||
if (!existingRole || existingRole !== importedRole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingText = extractComparableText(existing);
|
||||
const importedText = extractComparableText(imported);
|
||||
if (!existingText || !importedText || existingText !== importedText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingTimestamp = resolveComparableTimestamp(existing);
|
||||
const importedTimestamp = resolveComparableTimestamp(imported);
|
||||
if (existingTimestamp === undefined || importedTimestamp === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Math.abs(existingTimestamp - importedTimestamp) <= DEDUPE_TIMESTAMP_WINDOW_MS;
|
||||
}
|
||||
|
||||
function compareHistoryMessages(
|
||||
a: { message: unknown; order: number },
|
||||
b: { message: unknown; order: number },
|
||||
): number {
|
||||
const aTimestamp = resolveComparableTimestamp(a.message);
|
||||
const bTimestamp = resolveComparableTimestamp(b.message);
|
||||
if (aTimestamp !== undefined && bTimestamp !== undefined && aTimestamp !== bTimestamp) {
|
||||
return aTimestamp - bTimestamp;
|
||||
}
|
||||
if (aTimestamp !== undefined && bTimestamp === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (aTimestamp === undefined && bTimestamp !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
return a.order - b.order;
|
||||
}
|
||||
|
||||
function parseClaudeCliHistoryEntry(
|
||||
entry: ClaudeCliProjectEntry,
|
||||
cliSessionId: string,
|
||||
toolNameRegistry: ToolNameRegistry,
|
||||
): TranscriptLikeMessage | null {
|
||||
if (entry.isSidechain === true || !entry.message || typeof entry.message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const type = typeof entry.type === "string" ? entry.type : undefined;
|
||||
const role = typeof entry.message.role === "string" ? entry.message.role : undefined;
|
||||
if (type !== "user" && type !== "assistant") {
|
||||
return null;
|
||||
}
|
||||
if (role !== type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = resolveTimestampMs(entry.timestamp);
|
||||
const baseMeta = {
|
||||
importedFrom: CLAUDE_CLI_PROVIDER,
|
||||
cliSessionId,
|
||||
...(typeof entry.uuid === "string" && entry.uuid.trim() ? { externalId: entry.uuid } : {}),
|
||||
};
|
||||
|
||||
if (type === "user") {
|
||||
const content =
|
||||
typeof entry.message.content === "string" || Array.isArray(entry.message.content)
|
||||
? normalizeClaudeCliContent(entry.message.content, toolNameRegistry)
|
||||
: undefined;
|
||||
if (content === undefined) {
|
||||
return null;
|
||||
}
|
||||
return attachOpenClawTranscriptMeta(
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
...(timestamp !== undefined ? { timestamp } : {}),
|
||||
},
|
||||
baseMeta,
|
||||
) as TranscriptLikeMessage;
|
||||
}
|
||||
|
||||
const content =
|
||||
typeof entry.message.content === "string" || Array.isArray(entry.message.content)
|
||||
? normalizeClaudeCliContent(entry.message.content, toolNameRegistry)
|
||||
: undefined;
|
||||
if (content === undefined) {
|
||||
return null;
|
||||
}
|
||||
return attachOpenClawTranscriptMeta(
|
||||
{
|
||||
role: "assistant",
|
||||
content,
|
||||
api: "anthropic-messages",
|
||||
provider: CLAUDE_CLI_PROVIDER,
|
||||
...(typeof entry.message.model === "string" && entry.message.model.trim()
|
||||
? { model: entry.message.model }
|
||||
: {}),
|
||||
...(typeof entry.message.stop_reason === "string" && entry.message.stop_reason.trim()
|
||||
? { stopReason: entry.message.stop_reason }
|
||||
: {}),
|
||||
...(resolveClaudeCliUsage(entry.message.usage)
|
||||
? { usage: resolveClaudeCliUsage(entry.message.usage) }
|
||||
: {}),
|
||||
...(timestamp !== undefined ? { timestamp } : {}),
|
||||
},
|
||||
baseMeta,
|
||||
) as TranscriptLikeMessage;
|
||||
}
|
||||
|
||||
export function resolveClaudeCliSessionFilePath(params: {
|
||||
cliSessionId: string;
|
||||
homeDir?: string;
|
||||
}): string | undefined {
|
||||
const projectsDir = resolveClaudeProjectsDir(params.homeDir);
|
||||
let projectEntries: fs.Dirent[];
|
||||
try {
|
||||
projectEntries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const entry of projectEntries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const candidate = path.join(projectsDir, entry.name, `${params.cliSessionId}.jsonl`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function readClaudeCliSessionMessages(params: {
|
||||
cliSessionId: string;
|
||||
homeDir?: string;
|
||||
}): TranscriptLikeMessage[] {
|
||||
const filePath = resolveClaudeCliSessionFilePath(params);
|
||||
if (!filePath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages: TranscriptLikeMessage[] = [];
|
||||
const toolNameRegistry: ToolNameRegistry = new Map();
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(line) as ClaudeCliProjectEntry;
|
||||
const message = parseClaudeCliHistoryEntry(parsed, params.cliSessionId, toolNameRegistry);
|
||||
if (message) {
|
||||
messages.push(message);
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed external history entries.
|
||||
}
|
||||
}
|
||||
return coalesceClaudeCliToolMessages(messages);
|
||||
}
|
||||
|
||||
export function mergeImportedChatHistoryMessages(params: {
|
||||
localMessages: unknown[];
|
||||
importedMessages: unknown[];
|
||||
}): unknown[] {
|
||||
if (params.importedMessages.length === 0) {
|
||||
return params.localMessages;
|
||||
}
|
||||
const merged = params.localMessages.map((message, index) => ({ message, order: index }));
|
||||
let nextOrder = merged.length;
|
||||
for (const imported of params.importedMessages) {
|
||||
if (merged.some((existing) => isEquivalentImportedMessage(existing.message, imported))) {
|
||||
continue;
|
||||
}
|
||||
merged.push({ message: imported, order: nextOrder });
|
||||
nextOrder += 1;
|
||||
}
|
||||
merged.sort(compareHistoryMessages);
|
||||
return merged.map((entry) => entry.message);
|
||||
}
|
||||
|
||||
export function augmentChatHistoryWithCliSessionImports(params: {
|
||||
entry: SessionEntry | undefined;
|
||||
provider?: string;
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
*/
|
||||
|
||||
import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
|
||||
import {
|
||||
isToolResultContentType,
|
||||
resolveToolBlockArgs,
|
||||
} from "../../../../src/chat/tool-content.js";
|
||||
import type { NormalizedMessage, MessageContentItem } from "../types/chat-types.ts";
|
||||
|
||||
/**
|
||||
@@ -22,8 +26,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
Array.isArray(contentItems) &&
|
||||
contentItems.some((item) => {
|
||||
const x = item as Record<string, unknown>;
|
||||
const t = (typeof x.type === "string" ? x.type : "").toLowerCase();
|
||||
return t === "toolresult" || t === "tool_result";
|
||||
return isToolResultContentType(x.type);
|
||||
});
|
||||
|
||||
const hasToolName = typeof m.toolName === "string" || typeof m.tool_name === "string";
|
||||
@@ -42,7 +45,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
type: (item.type as MessageContentItem["type"]) || "text",
|
||||
text: item.text as string | undefined,
|
||||
name: item.name as string | undefined,
|
||||
args: item.args || item.arguments || item.input,
|
||||
args: resolveToolBlockArgs(item),
|
||||
}));
|
||||
} else if (typeof m.text === "string") {
|
||||
content = [{ type: "text", text: m.text }];
|
||||
|
||||
34
ui/src/ui/chat/tool-cards.test.ts
Normal file
34
ui/src/ui/chat/tool-cards.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "lit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractToolCards, renderToolCardSidebar } from "./tool-cards.ts";
|
||||
|
||||
describe("tool cards", () => {
|
||||
it("renders anthropic tool_use input details in tool cards", () => {
|
||||
const cards = extractToolCards({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_123",
|
||||
name: "Bash",
|
||||
input: { command: 'time claude -p "say ok"' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(cards).toHaveLength(1);
|
||||
expect(cards[0]).toMatchObject({
|
||||
kind: "call",
|
||||
name: "Bash",
|
||||
args: { command: 'time claude -p "say ok"' },
|
||||
});
|
||||
|
||||
const container = document.createElement("div");
|
||||
render(renderToolCardSidebar(cards[0]), container);
|
||||
|
||||
expect(container.textContent).toContain('time claude -p "say ok"');
|
||||
expect(container.textContent).toContain("Bash");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { html, nothing } from "lit";
|
||||
import {
|
||||
isToolCallContentType,
|
||||
isToolResultContentType,
|
||||
resolveToolBlockArgs,
|
||||
} from "../../../../src/chat/tool-content.js";
|
||||
import { icons } from "../icons.ts";
|
||||
import { formatToolDetail, resolveToolDisplay } from "../tool-display.ts";
|
||||
import type { ToolCard } from "../types/chat-types.ts";
|
||||
@@ -13,23 +18,20 @@ export function extractToolCards(message: unknown): ToolCard[] {
|
||||
const cards: ToolCard[] = [];
|
||||
|
||||
for (const item of content) {
|
||||
const kind = (typeof item.type === "string" ? item.type : "").toLowerCase();
|
||||
const isToolCall =
|
||||
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
|
||||
(typeof item.name === "string" &&
|
||||
(item.arguments != null || item.args != null || item.input != null));
|
||||
isToolCallContentType(item.type) ||
|
||||
(typeof item.name === "string" && resolveToolBlockArgs(item) != null);
|
||||
if (isToolCall) {
|
||||
cards.push({
|
||||
kind: "call",
|
||||
name: (item.name as string) ?? "tool",
|
||||
args: coerceArgs(item.arguments ?? item.args ?? item.input),
|
||||
args: coerceArgs(resolveToolBlockArgs(item)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of content) {
|
||||
const kind = (typeof item.type === "string" ? item.type : "").toLowerCase();
|
||||
if (kind !== "toolresult" && kind !== "tool_result") {
|
||||
if (!isToolResultContentType(item.type)) {
|
||||
continue;
|
||||
}
|
||||
const text = extractToolText(item);
|
||||
|
||||
@@ -453,46 +453,6 @@ describe("chat view", () => {
|
||||
expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg");
|
||||
});
|
||||
|
||||
it("renders anthropic tool_use input details in tool cards", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_123",
|
||||
name: "Bash",
|
||||
input: { command: 'time claude -p "say ok"' },
|
||||
},
|
||||
],
|
||||
timestamp: 1000,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
name: "Bash",
|
||||
tool_use_id: "toolu_123",
|
||||
content: "ok",
|
||||
},
|
||||
],
|
||||
timestamp: 1001,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain('time claude -p "say ok"');
|
||||
expect(container.textContent).toContain("Bash");
|
||||
});
|
||||
|
||||
it("keeps the persisted overview locale selected before i18n hydration finishes", async () => {
|
||||
const container = document.createElement("div");
|
||||
const props = createOverviewProps({
|
||||
|
||||
@@ -75,6 +75,7 @@ export default defineConfig({
|
||||
"extensions/**/*.test.ts",
|
||||
"test/**/*.test.ts",
|
||||
"ui/src/ui/app-chat.test.ts",
|
||||
"ui/src/ui/chat/**/*.test.ts",
|
||||
"ui/src/ui/views/agents-utils.test.ts",
|
||||
"ui/src/ui/views/channels.test.ts",
|
||||
"ui/src/ui/views/chat.test.ts",
|
||||
|
||||
@@ -4,6 +4,7 @@ export const unitTestIncludePatterns = [
|
||||
"src/**/*.test.ts",
|
||||
"test/**/*.test.ts",
|
||||
"ui/src/ui/app-chat.test.ts",
|
||||
"ui/src/ui/chat/**/*.test.ts",
|
||||
"ui/src/ui/views/agents-utils.test.ts",
|
||||
"ui/src/ui/views/channels.test.ts",
|
||||
"ui/src/ui/views/chat.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user