From 9a0af8ec0204b7d2de78480f928dfc90a2701721 Mon Sep 17 00:00:00 2001 From: wuzixuan06 Date: Tue, 27 Oct 2026 17:01:32 +0800 Subject: [PATCH 1/4] feat(ai): show persistent execution steps and render chat Markdown --- electron/ai-edition/chat-execution.test.ts | 165 ++ electron/ai-edition/chat-execution.ts | 150 ++ electron/ai-edition/chat-persistence.ts | 7 + .../ai-edition/chat-service.execution.test.ts | 161 ++ electron/ai-edition/chat-service.ts | 35 +- electron/ai-edition/deep-agent/service.ts | 93 +- electron/ipc/nativeBridge.ts | 1 + package-lock.json | 1598 ++++++++++++++++- package.json | 2 + .../ai-edition/ChatExecutionSteps.test.tsx | 43 + .../ai-edition/ChatExecutionSteps.tsx | 60 + .../ai-edition/ChatMarkdown.module.css | 12 + .../ai-edition/ChatMarkdown.test.tsx | 39 + src/components/ai-edition/ChatMarkdown.tsx | 38 + .../ai-edition/LeftPanel.stop.test.tsx | 71 +- src/components/ai-edition/LeftPanel.tsx | 35 +- src/i18n/locales/ar/editor.json | 10 +- src/i18n/locales/cs/editor.json | 10 +- src/i18n/locales/de/editor.json | 10 +- src/i18n/locales/en/editor.json | 10 +- src/i18n/locales/es/editor.json | 10 +- src/i18n/locales/fr/editor.json | 10 +- src/i18n/locales/it/editor.json | 10 +- src/i18n/locales/ja-JP/editor.json | 10 +- src/i18n/locales/ko-KR/editor.json | 10 +- src/i18n/locales/pt-BR/editor.json | 10 +- src/i18n/locales/ru/editor.json | 10 +- src/i18n/locales/tr/editor.json | 10 +- src/i18n/locales/vi/editor.json | 10 +- src/i18n/locales/zh-CN/editor.json | 10 +- src/i18n/locales/zh-TW/editor.json | 10 +- src/lib/ai-edition/chatExecution.ts | 31 + src/native/contracts.ts | 4 + .../architecture/ai-agent.md | 11 + .../testing/manual-e2e-checklist.md | 1 + 35 files changed, 2597 insertions(+), 110 deletions(-) create mode 100644 electron/ai-edition/chat-execution.test.ts create mode 100644 electron/ai-edition/chat-execution.ts create mode 100644 electron/ai-edition/chat-service.execution.test.ts create mode 100644 src/components/ai-edition/ChatExecutionSteps.test.tsx create mode 100644 src/components/ai-edition/ChatExecutionSteps.tsx create mode 100644 src/components/ai-edition/ChatMarkdown.module.css create mode 100644 src/components/ai-edition/ChatMarkdown.test.tsx create mode 100644 src/components/ai-edition/ChatMarkdown.tsx create mode 100644 src/lib/ai-edition/chatExecution.ts diff --git a/electron/ai-edition/chat-execution.test.ts b/electron/ai-edition/chat-execution.test.ts new file mode 100644 index 000000000..07844d700 --- /dev/null +++ b/electron/ai-edition/chat-execution.test.ts @@ -0,0 +1,165 @@ +import { once } from "node:events"; +import type { StructuredToolInterface } from "@langchain/core/tools"; +import { describe, expect, it, vi } from "vitest"; +import { createEmptyDocument } from "../../src/lib/ai-edition/schema"; +import { + createExecutionTracker, + executionSummary, + executionText, + toolExecutionStep, +} from "./chat-execution"; +import { buildTools } from "./deep-agent/service"; + +describe("execution metadata", () => { + it("keeps reasoning and repeated/concurrent invocation identities in observed order", () => { + const tracker = createExecutionTracker(vi.fn()); + tracker.thinking("**Read** the transcript"); + const first = toolExecutionStep("getTranscript", {}); + const second = toolExecutionStep("getTranscript", {}); + tracker.tool(first); + tracker.tool(second); + tracker.tool({ ...second, status: "failed", summary: "No transcript" }); + tracker.tool({ ...first, status: "succeeded" }); + tracker.thinking("Try another source"); + const steps = tracker.finish(false); + expect(steps.map((step) => step.kind)).toEqual(["reasoning", "tool", "tool", "reasoning"]); + expect(steps[1]).toMatchObject({ id: first.id, status: "succeeded" }); + expect(steps[2]).toMatchObject({ id: second.id, status: "failed" }); + expect(first.id).not.toBe(second.id); + }); + it("keeps sensitive arbitrary strings out of argument and result metadata", () => { + const step = toolExecutionStep("addAnnotation", { + apiKey: "private", + password: 12345, + text: "private", + assetId: "asset_1", + startSec: 2, + words: ["private"], + }); + expect(step.arguments).toBe("assetId: asset_1, startSec: 2, words: 1"); + expect(executionText("Bearer private apiKey=private sk-abcdefghijklmnop")).not.toContain( + "private", + ); + expect( + executionSummary({ + ok: true, + resultJson: JSON.stringify({ raw: "private", words: ["private"], count: 1 }), + }), + ).toBe("words: 1, count: 1"); + expect(executionSummary({ ok: false, resultJson: '{"error":"No transcript"}' })).toBe( + "No transcript", + ); + expect(executionText("x".repeat(2000))).toHaveLength(1000); + }); + it("uses the real executor verdict and pairs each repeated call by id", async () => { + const events = vi.fn(); + const holder = { current: createEmptyDocument({ projectId: "steps", title: "Test" }) }; + const tools = buildTools( + holder, + { + executionStep: events, + toolStart: vi.fn(), + toolEnd: vi.fn(), + text: vi.fn(), + thinking: vi.fn(), + error: vi.fn(), + }, + true, + ); + const tool = tools.find((entry) => entry.name === "getTranscript")! as StructuredToolInterface; + await tool.invoke({ assetId: "missing" }); + await tool.invoke({ assetId: "missing" }); + const steps = events.mock.calls.map(([step]) => step); + expect(steps).toHaveLength(4); + expect(steps[1]).toMatchObject({ + id: steps[0].id, + status: "failed", + summary: expect.stringMatching(/asset|transcript/i), + durationMs: expect.any(Number), + }); + expect(steps[3]).toMatchObject({ id: steps[2].id, status: "failed" }); + expect(steps[0].id).not.toBe(steps[2].id); + }); +}); + +it("reports repeated real SDK calls and schema/unknown-tool failures once per invocation", async () => { + const { createServer } = await import("node:http"); + + const { invokeOpenScreenAgent } = await import("./deep-agent/service"); + const steps = new Map< + string, + import("../../src/lib/ai-edition/chatExecution").ToolExecutionStep + >(); + let requests = 0; + const server = createServer(async (req, res) => { + for await (const _ of req) { + /* consume local fixture request */ + } + res.writeHead(200, { "Content-Type": "text/event-stream" }); + const first = ++requests === 1; + const delta = first + ? { + role: "assistant", + tool_calls: [ + "getCurrentDocument", + "getCurrentDocument", + "getTranscript", + "nonexistentTool", + ].map((name, index) => ({ + index, + id: `call_${index}`, + type: "function", + function: { + name, + arguments: JSON.stringify(name === "getTranscript" ? { assetId: 123 } : {}), + }, + })), + } + : { role: "assistant", content: "Done" }; + for (const [value, finish] of [ + [delta, null], + [{}, first ? "tool_calls" : "stop"], + ]) + res.write( + `data: ${JSON.stringify({ id: "chat_fixture", object: "chat.completion.chunk", created: 1, model: "fixture", choices: [{ index: 0, delta: value, finish_reason: finish }] })}\n\n`, + ); + res.end("data: [DONE]\n\n"); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing local port"); + try { + const result = await invokeOpenScreenAgent({ + document: createEmptyDocument({ projectId: "sdk_steps", title: "Test" }), + model: { + provider: "openai-compatible", + model: "fixture", + apiKey: "local-only", + baseUrl: `http://127.0.0.1:${address.port}/v1`, + maxRetries: 0, + }, + history: [], + userMessage: "Inspect", + sink: { + executionStep: (step) => steps.set(step.id, step), + text: vi.fn(), + thinking: vi.fn(), + toolStart: vi.fn(), + toolEnd: vi.fn(), + error: vi.fn(), + }, + }); + expect(result.text).toBe("Done"); + expect([...steps.values()].map((step) => [step.name, step.status])).toEqual([ + ["getCurrentDocument", "succeeded"], + ["getCurrentDocument", "succeeded"], + ["getTranscript", "failed"], + ["nonexistentTool", "failed"], + ]); + expect([...steps.values()][2].summary).toMatch(/schema|input|assetId/i); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + } +}); diff --git a/electron/ai-edition/chat-execution.ts b/electron/ai-edition/chat-execution.ts new file mode 100644 index 000000000..0d6fe7393 --- /dev/null +++ b/electron/ai-edition/chat-execution.ts @@ -0,0 +1,150 @@ +import { randomUUID } from "node:crypto"; +import { + type ChatExecutionStep, + type ToolExecutionStep, + updateExecutionStep, +} from "../../src/lib/ai-edition/chatExecution"; + +/** Keep display metadata bounded; raw provider payloads and tool results never enter the sidecar. */ +export function executionText(value: string, limit = 1000): string { + return value + .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]") + .replace(/\b(?:sk-|gh[pousr]_)[A-Za-z0-9_-]{8,}/g, "[redacted]") + .replace( + /\b(?:api[_-]?key|authorization|password|secret|token)\s*[=:]\s*[^\s,;]+/gi, + "[redacted]", + ) + .slice(0, limit); +} + +export function toolExecutionStep(name: string, args: unknown): ToolExecutionStep { + const summaries: string[] = []; + if (args && typeof args === "object") { + // Describe location and scale, not arbitrary text, headers, paths or credentials. + for (const [key, value] of Object.entries(args)) { + if (/api.?key|authorization|password|secret|token/i.test(key)) continue; + if (Array.isArray(value)) summaries.push(`${key}: ${value.length}`); + else if (typeof value === "number" || typeof value === "boolean") + summaries.push(`${key}: ${value}`); + else if (["assetId", "clipId", "id"].includes(key) && typeof value === "string") + summaries.push(`${key}: ${value}`); + } + } + return { + id: randomUUID(), + kind: "tool", + name: executionText(name, 100), + arguments: executionText(summaries.join(", ")), + status: "running", + startedAt: Date.now(), + }; +} + +export function createExecutionTracker(emit: (step: ChatExecutionStep) => void) { + let steps: ChatExecutionStep[] = []; + let reasoningId: string | undefined; + const update = (step: ChatExecutionStep) => { + steps = updateExecutionStep(steps, step); + emit(step); + }; + return { + tool(step: ToolExecutionStep) { + reasoningId = undefined; + update(step); + }, + thinking(delta: string) { + const previous = steps.find((step) => step.id === reasoningId); + const text = previous?.kind === "reasoning" ? previous.text : ""; + const step: ChatExecutionStep = { + id: reasoningId ?? randomUUID(), + kind: "reasoning", + startedAt: previous?.startedAt ?? Date.now(), + text: executionText(text + delta, 32768), + }; + reasoningId = step.id; + update(step); + }, + finish(stopped: boolean) { + for (const step of steps) { + if (step.kind === "tool" && step.status === "running") + update({ + ...step, + status: stopped ? "stopped" : "failed", + durationMs: Math.max(0, Date.now() - step.startedAt), + }); + } + return steps; + }, + snapshot: () => steps, + }; +} + +export function executionSummary(result: { + ok: boolean; + summary?: string; + resultJson: string; +}): string { + if (result.summary) return executionText(result.summary); + try { + const data: unknown = JSON.parse(result.resultJson); + if (data && typeof data === "object") { + if (!result.ok && "error" in data && typeof data.error === "string") + return executionText(data.error); + const counts = Object.entries(data) + .filter( + ([key, value]) => + /^[a-zA-Z][a-zA-Z0-9]*$/.test(key) && + !/api.?key|authorization|password|secret|token/i.test(key) && + (Array.isArray(value) || typeof value === "number" || typeof value === "boolean"), + ) + .slice(0, 8) + .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.length : value}`); + return executionText(counts.join(", ")); + } + } catch { + /* Raw tool results are intentionally never displayed or persisted. */ + } + return ""; +} + +/** Reconcile announced calls with executor verdicts, including calls rejected before execution. */ +export function createToolExecutionObserver(emit: (step: ToolExecutionStep) => void) { + const calls = new Map(); + const begin = (callId: string, name: string, args: unknown) => { + const existing = calls.get(callId); + if (existing) return existing; + const step = toolExecutionStep(name, args); + calls.set(callId, step); + emit(step); + return step; + }; + return { + reset() { + calls.clear(); + }, + begin, + publish(step: ToolExecutionStep) { + const previous = calls.get(step.id) ?? begin(step.id, step.name, {}); + const next = { + ...step, + id: previous.id, + startedAt: previous.startedAt, + arguments: step.arguments || previous.arguments, + }; + calls.set(step.id, next); + emit(next); + }, + result(callId: string, content: string) { + const previous = calls.get(callId); + if (!previous || previous.status !== "running") return; + const step: ToolExecutionStep = { + ...previous, + status: "failed", + summary: executionText(content), + durationMs: Math.max(0, Date.now() - previous.startedAt), + }; + calls.set(callId, step); + emit(step); + }, + }; +} diff --git a/electron/ai-edition/chat-persistence.ts b/electron/ai-edition/chat-persistence.ts index 879e77009..c309558eb 100644 --- a/electron/ai-edition/chat-persistence.ts +++ b/electron/ai-edition/chat-persistence.ts @@ -7,6 +7,10 @@ import { existsSync, readFileSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; +import { + executionStepSchema, + restoredExecutionSteps, +} from "../../src/lib/ai-edition/chatExecution"; import { renameWithRetry } from "../atomic-rename"; const toolCallSchema = z.object({ name: z.string(), summary: z.string() }); @@ -55,6 +59,7 @@ const optionalUsageSchema = z.unknown().transform((value) => { return parsed.success ? parsed.data : undefined; }); const messageSchema = z.object({ + executionSteps: z.array(executionStepSchema).optional().catch(undefined), id: z.string().min(1), role: z.enum(["user", "assistant"]), content: z.string(), @@ -112,6 +117,8 @@ export class ChatPersistence { if (session.compaction && session.compaction.coveredCount > session.messages.length) continue; seen.add(session.id); + for (const message of session.messages) + message.executionSteps = restoredExecutionSteps(message.executionSteps); sessions.push(session); } return sessions; diff --git a/electron/ai-edition/chat-service.execution.test.ts b/electron/ai-edition/chat-service.execution.test.ts new file mode 100644 index 000000000..34439505e --- /dev/null +++ b/electron/ai-edition/chat-service.execution.test.ts @@ -0,0 +1,161 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { createEmptyDocument } from "../../src/lib/ai-edition/schema"; +import { createExecutionTracker, toolExecutionStep } from "./chat-execution"; +import { ChatPersistence } from "./chat-persistence"; + +vi.mock("./deep-agent/service", () => ({ invokeOpenScreenAgent: vi.fn() })); + +import { + configureChatPersistence, + createSession, + deleteSession, + flushChatPersistence, + runChat, + selectSession, +} from "./chat-service"; +import { invokeOpenScreenAgent } from "./deep-agent/service"; +import type { LlmConfigStore } from "./llm-config-store"; + +let root: string; +const config = { + getConfig: () => ({ provider: "openai", model: "fixture" }), + getCredential: () => ({ value: "test" }), +} as unknown as LlmConfigStore; +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), "openscreen-execution-")); + configureChatPersistence(root); +}); +afterEach(async () => { + await flushChatPersistence(); + await rm(root, { recursive: true, force: true }); + vi.clearAllMocks(); +}); + +it.each([ + "success", + "failed", + "stopped", +])("persists ordered %s steps on their originating turn across session changes and restart", async (mode) => { + const session = createSession("p"); + const other = createSession("p"); + vi.mocked(invokeOpenScreenAgent).mockImplementationOnce(async (args) => { + args.sink.thinking("**Inspect** the project"); + const first = toolExecutionStep("getTranscript", { assetId: "a" }); + const second = toolExecutionStep("getTranscript", { assetId: "b" }); + args.sink.executionStep?.(first); + args.sink.executionStep?.(second); + args.sink.executionStep?.({ + ...second, + status: "failed", + summary: "No transcript", + durationMs: 25, + }); + args.sink.executionStep?.({ ...first, status: "succeeded", durationMs: 50 }); + args.sink.thinking("Choose the available source"); + if (mode === "stopped") args.sink.executionStep?.(toolExecutionStep("getCurrentDocument", {})); + selectSession("p", other.id); + return { + text: mode === "failed" ? "" : "## Result\n\n- Done", + reason: mode === "failed" ? "Provider failed" : undefined, + document: args.document, + mutated: false, + cancelled: mode === "stopped", + }; + }); + const source = " indented\n\n```ts\n const x = 1;\n```\n"; + const result = await runChat( + "p", + session.id, + source, + config, + createEmptyDocument({ projectId: "p", title: "test" }), + ); + expect(vi.mocked(invokeOpenScreenAgent).mock.lastCall?.[0].userMessage).toBe(source); + const message = mode === "failed" ? result.userMessage : result.assistantMessage; + expect(message?.executionSteps?.map((step) => step.kind)).toEqual( + mode === "stopped" + ? ["reasoning", "tool", "tool", "reasoning", "tool"] + : ["reasoning", "tool", "tool", "reasoning"], + ); + expect(message?.executionSteps?.[2]).toMatchObject({ + status: "failed", + summary: "No transcript", + }); + if (mode === "stopped") + expect(message?.executionSteps?.at(-1)).toMatchObject({ status: "stopped" }); + await flushChatPersistence(); + configureChatPersistence(root); + const history = selectSession("p", session.id)!; + expect(history.messages[0].content).toBe(source); + expect(history.messages.at(-1)?.executionSteps).toEqual(message?.executionSteps); + expect(selectSession("p", other.id)?.messages).toEqual([]); + const persisted = await readFile(join(root, "p.chat.json"), "utf8"); + expect(persisted).not.toContain("resultJson"); +}); + +it("does not recreate execution history after the originating session is deleted", async () => { + const session = createSession("p"); + vi.mocked(invokeOpenScreenAgent).mockImplementationOnce(async (args) => { + args.sink.thinking("Inspect"); + deleteSession("p", session.id); + args.sink.executionStep?.(toolExecutionStep("getTranscript", {})); + return { text: "Done", document: args.document, mutated: false }; + }); + const result = await runChat("p", session.id, "hello", config); + expect(result.success).toBe(false); + await flushChatPersistence(); + configureChatPersistence(root); + expect(selectSession("p", session.id)).toBeNull(); +}); + +it("restores an interrupted running step and tolerates legacy or malformed metadata", async () => { + const store = new ChatPersistence(root); + const tracker = createExecutionTracker(vi.fn()); + tracker.tool(toolExecutionStep("getTranscript", {})); + const base = { id: "a", projectId: "p", title: "A", createdAt: "now" }; + await store.write("p", [ + { + ...base, + messages: [ + { id: "m1", role: "user", content: "legacy", createdAt: "now" }, + { + id: "m2", + role: "assistant", + content: "partial", + createdAt: "now", + executionSteps: tracker.snapshot(), + }, + ], + }, + ]); + const history = store.read("p")[0]; + expect(history.messages[0].executionSteps).toBeUndefined(); + expect(history.messages[1].executionSteps?.[0]).toMatchObject({ status: "stopped" }); + await writeFile( + store.fileFor("p"), + JSON.stringify({ + version: 1, + projectId: "p", + sessions: [ + { + ...base, + messages: [ + { + id: "old", + role: "assistant", + content: "Retained message", + createdAt: "now", + executionSteps: [{ kind: "invalid" }], + }, + ], + }, + ], + }), + ); + const malformed = store.read("p")[0].messages[0]; + expect(malformed.content).toBe("Retained message"); + expect(malformed.executionSteps).toBeUndefined(); +}); diff --git a/electron/ai-edition/chat-service.ts b/electron/ai-edition/chat-service.ts index dc1030ee3..a367e2695 100644 --- a/electron/ai-edition/chat-service.ts +++ b/electron/ai-edition/chat-service.ts @@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto"; import { HumanMessage, SystemMessage } from "@langchain/core/messages"; +import type { ChatExecutionStep, ToolExecutionStep } from "../../src/lib/ai-edition/chatExecution"; import { type AxcutTimelineOperation, applyTimelineOperation, @@ -33,6 +34,7 @@ import { compactionSplitIndex, DEFAULT_BUDGET_TOKENS, } from "./chat-compaction"; +import { createExecutionTracker } from "./chat-execution"; import { ChatPersistence } from "./chat-persistence"; import type { CursorTelemetryReader } from "./deep-agent/service"; import type { DocumentService } from "./document-service"; @@ -299,6 +301,7 @@ export function deleteSession(projectId: string, sessionId: string): boolean { export interface ChatEventSink { usage?: (usage: AgentTurnUsage) => void; + executionStep?: (step: ChatExecutionStep) => void; /** Streamed text delta from the model. */ text?: (delta: string) => void; /** Streamed delta from the model's reasoning block (Anthropic/MiniMax @@ -334,6 +337,7 @@ const noop = () => undefined; /** ponytail: zero-config sink that swallows every event. */ const NOOP_SINK: Required = { usage: noop, + executionStep: noop, text: noop, thinking: noop, toolStart: noop, @@ -439,9 +443,29 @@ export async function runChat( const appliedToolCalls: AiEditionToolCallSummary[] = []; + const originatingSession = session; + const isOriginValid = () => + sessionsByProject.get(projectId) === sessions && + sessions.get(originatingSession.id) === originatingSession && + originatingSession.messages.includes(userMessage); + let pendingSave: ReturnType | undefined; + const execution = createExecutionTracker((step) => { + if (!isOriginValid()) return; + userMessage.executionSteps = execution.snapshot(); + emit.executionStep(step); + if (step.kind === "tool") persistProject(projectId); + else if (!pendingSave) + pendingSave = setTimeout(() => { + pendingSave = undefined; + if (isOriginValid()) persistProject(projectId); + }, 200); + }); let acceptingEvents = true; const canEmit = () => acceptingEvents && !env.signal?.aborted; const agentSink = { + executionStep: (step: ToolExecutionStep) => { + if (acceptingEvents && isOriginValid()) execution.tool(step); + }, document: (document: AxcutDocument) => { if (editsAllowed && canEmit()) emit.document(document); }, @@ -449,7 +473,10 @@ export async function runChat( if (canEmit()) emit.text(delta); }, thinking: (delta: string) => { - if (canEmit()) emit.thinking(delta); + if (canEmit()) { + emit.thinking(delta); + execution.thinking(delta); + } }, usage: (usage: AgentTurnUsage) => { if (canEmit()) emit.usage(usage); @@ -492,6 +519,8 @@ export async function runChat( checkShaderEffect: env.checkShaderEffect, }); + const executionSteps = execution.finish(Boolean(result.cancelled)); + if (pendingSave) clearTimeout(pendingSave); acceptingEvents = false; const durationMs = Math.max(0, Math.round(performance.now() - startedAt)); @@ -514,6 +543,7 @@ export async function runChat( } if (!result.text && !result.cancelled) { + persistProject(projectId); // ponytail: surface the deep-agent's diagnostic so the user can see // *why* the model produced no text (e.g. MiniMax streaming only // thinking blocks, or a non-text content shape that our extractor @@ -523,6 +553,7 @@ export async function runChat( originatingMessage.durationMs = durationMs; persistProject(projectId); return { + userMessage, success: false, error: result.reason ?? "Empty response from model.", // The message is stored all the same, and can be rewound to retry it. @@ -532,7 +563,9 @@ export async function runChat( }; } + delete userMessage.executionSteps; const assistantMessage: AiEditionChatMessage = { + executionSteps, id: randomUUID(), role: "assistant", content: result.text, diff --git a/electron/ai-edition/deep-agent/service.ts b/electron/ai-edition/deep-agent/service.ts index 67c3b7df7..6bfed5ed8 100644 --- a/electron/ai-edition/deep-agent/service.ts +++ b/electron/ai-edition/deep-agent/service.ts @@ -1,3 +1,4 @@ +import type { ToolRunnableConfig } from "@langchain/core/tools"; // ponytail: port of axcut's AxcutDeepAgentService (apps/server/src/services/ // axcut-deep-agent.ts). Wraps LangChain's `createAgent` (LangGraph ReAct graph) // with our existing document tools and emits agent.text / agent.toolStart / @@ -32,6 +33,7 @@ import { CAPTION_WORDS_PER_LINE_MAX, CAPTION_WORDS_PER_LINE_MIN, } from "../../../src/lib/ai-edition/captions/wordsPerLine"; +import type { ToolExecutionStep } from "../../../src/lib/ai-edition/chatExecution"; import { AUDIO_FADE_MAX_MS } from "../../../src/lib/ai-edition/document/audioTracks"; import { VOCABULARY_MAX_CHARS } from "../../../src/lib/ai-edition/document/transcript"; import type { AxcutDocument } from "../../../src/lib/ai-edition/schema"; @@ -101,6 +103,12 @@ import { setCaptionSettingsArgs, setVocabularyArgs, } from "../caption-tools"; +import { + createToolExecutionObserver, + executionSummary, + executionText, + toolExecutionStep, +} from "../chat-execution"; import { setEditorSettingsArgs } from "../editor-settings-tool"; import { abortable } from "./cancellation"; import { @@ -114,6 +122,7 @@ import { createTurnUsageAccumulator } from "./usage"; export interface OpenScreenAgentSink { /** Latest cumulative usage from completed model calls; never sum stream chunks. */ usage?: (usage: AgentTurnUsage) => void; + executionStep?: (step: ToolExecutionStep) => void; text: (delta: string) => void; /** Streaming delta from the model's reasoning block (Anthropic/MiniMax * thinking). Provider-agnostic — for providers without thinking this is @@ -384,18 +393,45 @@ function documentTool( inTurn: TurnQueue, ) { return tool( - (args: z.infer) => + (args: z.infer, config?: ToolRunnableConfig) => inTurn(async () => { runtime.signal?.throwIfAborted(); + const step = toolExecutionStep(name, args); + step.id = config?.toolCall?.id ?? step.id; + sink.executionStep?.(step); sink.toolStart(name, args); - const execution = await runDocumentTool(holder.current, name, args, editsAllowed, runtime); - runtime.signal?.throwIfAborted(); - if (execution.document) { - holder.current = execution.document; - sink.document?.(execution.document); + try { + const execution = await runDocumentTool( + holder.current, + name, + args, + editsAllowed, + runtime, + ); + runtime.signal?.throwIfAborted(); + if (execution.document) { + holder.current = execution.document; + sink.document?.(execution.document); + } + sink.toolEnd(name, execution.ok, execution.summary); + sink.executionStep?.({ + ...step, + status: execution.ok ? "succeeded" : "failed", + durationMs: Math.max(0, Date.now() - step.startedAt), + summary: executionSummary(execution), + }); + return execution.resultJson; + } catch (error) { + sink.executionStep?.({ + ...step, + status: runtime.signal?.aborted ? "stopped" : "failed", + durationMs: Math.max(0, Date.now() - step.startedAt), + summary: runtime.signal?.aborted + ? undefined + : executionText(error instanceof Error ? error.message : String(error)), + }); + throw error; } - sink.toolEnd(name, execution.ok, execution.summary); - return execution.resultJson; }), { name, description: TOOL_DESCRIPTIONS[name], schema }, ); @@ -700,6 +736,7 @@ export interface InvokeResult { export async function invokeOpenScreenAgent(args: InvokeArgs): Promise { const { document, model, history, userMessage, sink } = args; const editsAllowed = args.editsAllowed !== false; + const toolExecutions = createToolExecutionObserver((step) => sink.executionStep?.(step)); const lifetime = new AbortController(); const signal = args.signal ? AbortSignal.any([args.signal, lifetime.signal]) : lifetime.signal; let finalText = ""; @@ -717,13 +754,18 @@ export async function invokeOpenScreenAgent(args: InvokeArgs): Promise | undefined; const name = typeof event.name === "string" ? (event.name as string) : ""; + if (eventType === "on_chat_model_start") toolExecutions.reset(); + if (eventType === "on_chat_model_end") { + const output = data?.output as + | { tool_calls?: Array<{ id?: string; name: string; args: unknown }> } + | undefined; + for (const call of output?.tool_calls ?? []) + if (call.id) toolExecutions.begin(call.id, call.name, call.args); + } + if (eventType === "on_chain_end") { + const output = data?.output as + | { messages?: Array<{ tool_call_id?: string; status?: string; content?: unknown }> } + | undefined; + // The SDK omits status on schema errors. Successful document tools already emitted a terminal verdict. + for (const message of output?.messages ?? []) + if (message.tool_call_id) + toolExecutions.result(message.tool_call_id, messageContentToText(message.content)); + } if (eventType === "on_chat_model_stream") { const chunk = data?.chunk as Record | undefined; if (chunk) chatModelChunks.push(chunk); @@ -816,11 +875,11 @@ export async function invokeOpenScreenAgent(args: InvokeArgs): Promise send({ ...identity, kind: "executionStep", sessionId, step }), document: (document) => send({ ...identity, kind: "document", sessionId, document }), text: (delta) => send({ ...identity, kind: "text", sessionId, delta }), thinking: (delta) => send({ ...identity, kind: "thinking", sessionId, delta }), diff --git a/package-lock.json b/package-lock.json index d39017104..50fec0675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,8 +45,10 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.6.0", + "react-markdown": "10.1.0", "react-resizable-panels": "^3.0.6", "react-rnd": "^10.5.3", + "remark-gfm": "4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", @@ -4542,7 +4544,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -4574,9 +4575,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -4587,6 +4596,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -4610,11 +4628,19 @@ "@types/node": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -4662,6 +4688,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4877,6 +4909,12 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -5519,6 +5557,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -5891,6 +5939,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -5918,6 +5976,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6123,6 +6221,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -6346,6 +6454,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6446,7 +6567,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6466,6 +6586,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -7133,6 +7266,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -7292,6 +7435,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -7950,6 +8099,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hono": { "version": "4.13.11", "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.11.tgz", @@ -8005,6 +8194,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -8157,6 +8356,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.7.2", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", @@ -8175,6 +8380,30 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -8202,6 +8431,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8233,6 +8472,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-network-error": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", @@ -8254,6 +8503,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -8821,6 +9082,16 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8883,6 +9154,16 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -8906,103 +9187,971 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.4.tgz", + "integrity": "sha512-z4B5c6jrnJ1ltOQemhWYPBSAo6y42Z8Gt2yEgfgNTMcLSU59B8bfj8Z8AnAlJrBGLnqkdAUZ7jdDEqQw3KuipQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mediabunny": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.40.1.tgz", - "integrity": "sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==", - "license": "MPL-2.0", - "workspaces": [ - "packages/*" - ], + "node_modules/mdast-util-from-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", + "license": "MIT", "dependencies": { - "@types/dom-mediacapture-transform": "^0.1.11", - "@types/dom-webcodecs": "0.1.13" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=4.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-db": { + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.1.tgz", + "integrity": "sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mediabunny": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.40.1.tgz", + "integrity": "sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==", + "license": "MPL-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", @@ -9609,6 +10758,31 @@ "node": ">=8" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -10107,6 +11281,16 @@ "signal-exit": "^3.0.2" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/prosemirror-changeset": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", @@ -10464,6 +11648,33 @@ "license": "MIT", "peer": true }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -10650,6 +11861,72 @@ "node": ">=8" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11247,6 +12524,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -11346,6 +12633,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -11388,6 +12689,24 @@ "node": ">=8" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -11823,6 +13142,26 @@ "node": ">=20" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -11949,6 +13288,93 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz", + "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", @@ -12132,6 +13558,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", @@ -12686,6 +14140,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 0236b8ef1..e3ee16217 100644 --- a/package.json +++ b/package.json @@ -137,8 +137,10 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.6.0", + "react-markdown": "10.1.0", "react-resizable-panels": "^3.0.6", "react-rnd": "^10.5.3", + "remark-gfm": "4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", diff --git a/src/components/ai-edition/ChatExecutionSteps.test.tsx b/src/components/ai-edition/ChatExecutionSteps.test.tsx new file mode 100644 index 000000000..172a0c639 --- /dev/null +++ b/src/components/ai-edition/ChatExecutionSteps.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment jsdom +import "@testing-library/jest-dom"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, expect, it, vi } from "vitest"; +import type { ChatExecutionStep } from "@/lib/ai-edition/chatExecution"; +import { ChatExecutionSteps } from "./ChatExecutionSteps"; + +vi.mock("@/contexts/I18nContext", () => ({ useScopedT: () => (key: string) => key })); +afterEach(cleanup); +it("renders ordered collapsed details, failures and separate provider reasoning", () => { + const steps: ChatExecutionStep[] = [ + { id: "r", startedAt: 0, kind: "reasoning", text: "**Inspect** the clip" }, + { + id: "a", + startedAt: 1, + kind: "tool", + name: "getTranscript", + arguments: "assetId: a", + status: "running", + }, + { + id: "b", + startedAt: 2, + kind: "tool", + name: "getTranscript", + arguments: "assetId: b", + status: "failed", + summary: "No transcript", + durationMs: 240, + }, + ]; + const view = render(); + expect(view.container.querySelectorAll("details")).toHaveLength(3); + expect(view.container.querySelector("details[open]")).toBeNull(); + expect(screen.getByText(/chat.stepStatus.failed/)).toHaveTextContent("0.2s"); + view.rerender( + (step.id === "a" ? { ...step, status: "succeeded" } : step))} + />, + ); + expect(view.container.querySelectorAll("details")).toHaveLength(3); + expect(screen.getByText(/chat.stepStatus.succeeded/)).toBeInTheDocument(); +}); diff --git a/src/components/ai-edition/ChatExecutionSteps.tsx b/src/components/ai-edition/ChatExecutionSteps.tsx new file mode 100644 index 000000000..647bc206e --- /dev/null +++ b/src/components/ai-edition/ChatExecutionSteps.tsx @@ -0,0 +1,60 @@ +import { useScopedT } from "@/contexts/I18nContext"; +import type { ChatExecutionStep } from "@/lib/ai-edition/chatExecution"; +import { ChatMarkdown } from "./ChatMarkdown"; + +export function ChatExecutionSteps({ steps }: { steps?: ChatExecutionStep[] }) { + const t = useScopedT("editor"); + if (!steps?.length) return null; + return ( +
+ {steps.map((step) => ( +
+ + {step.kind === "reasoning" ? ( + t("chat.stepReasoning") + ) : ( + <> + + {step.status === "succeeded" + ? "✓" + : step.status === "failed" + ? "✗" + : step.status === "stopped" + ? "■" + : "…"} + {" "} + {step.name} + {step.arguments ? ` · ${step.arguments}` : ""} + {" · "} + {t(`chat.stepStatus.${step.status}`)} + {step.durationMs !== undefined ? ` · ${(step.durationMs / 1000).toFixed(1)}s` : ""} + + )} + + {step.kind === "tool" && step.arguments ? ( +
{step.arguments}
+ ) : null} + {step.kind === "reasoning" ? ( + + ) : step.summary ? ( + + ) : null} +
+ ))} +
+ ); +} diff --git a/src/components/ai-edition/ChatMarkdown.module.css b/src/components/ai-edition/ChatMarkdown.module.css new file mode 100644 index 000000000..6e4caa3a3 --- /dev/null +++ b/src/components/ai-edition/ChatMarkdown.module.css @@ -0,0 +1,12 @@ +.markdown { min-width: 0; overflow-wrap: anywhere; font: 13px/1.6 var(--font-body); } +.markdown > :first-child { margin-top: 0; } +.markdown > :last-child { margin-bottom: 0; } +.markdown p { white-space: pre-wrap; } +.markdown pre { overflow-x: auto; padding: 10px; border-radius: 6px; background: var(--surface-3); color: var(--fg); white-space: pre; } +.markdown code { font-family: monospace; } +.markdown h1, .markdown h2, .markdown h3 { font-size: 1.1em; margin: 12px 0 6px; } +.markdown ul, .markdown ol { padding-left: 22px; } +.markdown blockquote { margin-left: 0; padding-left: 12px; border-left: 2px solid var(--muted); } +.markdown table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; } +.markdown th, .markdown td { padding: 5px 8px; border: 1px solid var(--border, #444); } +.markdown a { color: var(--accent, #7cb8ff); text-decoration: underline; } diff --git a/src/components/ai-edition/ChatMarkdown.test.tsx b/src/components/ai-edition/ChatMarkdown.test.tsx new file mode 100644 index 000000000..34b5ccf01 --- /dev/null +++ b/src/components/ai-edition/ChatMarkdown.test.tsx @@ -0,0 +1,39 @@ +// @vitest-environment jsdom +import "@testing-library/jest-dom"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, expect, it, vi } from "vitest"; +import { ChatMarkdown } from "./ChatMarkdown"; + +afterEach(cleanup); +it("renders common Markdown and GFM tables without changing code indentation", () => { + const { container } = render( + Quote\n\n```ts\n const x = 1;\n```\n\n| A | B |\n| - | - |\n| 1 | 2 |" + } + />, + ); + expect(screen.getByRole("heading")).toHaveTextContent("Heading"); + expect(container.querySelector("strong")).toHaveTextContent("Bold"); + expect(screen.getByRole("listitem")).toHaveTextContent("Item"); + expect(screen.getByRole("table")).toHaveTextContent("A"); + expect(container.querySelector("pre code")?.textContent).toBe(" const x = 1;\n"); +}); +it("rejects scripts, raw HTML, dangerous links, local navigation and remote images", () => { + const { container } = render( + alert(1)\n\n\n\n[bad](javascript:alert%281%29) [local](file:///etc/passwd) [relative](../secret) ![hidden](https://tracker.invalid/pixel)" + } + />, + ); + expect(container.querySelector("script, img, iframe")).toBeNull(); + expect(container.querySelector("a")).toBeNull(); +}); +it("opens an explicit safe link through the desktop URL handler", () => { + const openExternalUrl = vi.fn().mockResolvedValue({ success: true }); + window.electronAPI = { openExternalUrl } as unknown as typeof window.electronAPI; + render(); + fireEvent.click(screen.getByRole("link", { name: "Project" })); + expect(openExternalUrl).toHaveBeenCalledWith("https://getopenscreen.com/"); +}); diff --git a/src/components/ai-edition/ChatMarkdown.tsx b/src/components/ai-edition/ChatMarkdown.tsx new file mode 100644 index 000000000..46e02f41a --- /dev/null +++ b/src/components/ai-edition/ChatMarkdown.tsx @@ -0,0 +1,38 @@ +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import styles from "./ChatMarkdown.module.css"; + +/** Render Markdown as React elements; HTML is skipped and unsafe URL schemes are rejected. */ +export function ChatMarkdown({ text }: { text: string }) { + return ( +
+ {alt}, + a: ({ href, children }) => + href && /^(https?:\/\/|mailto:)/i.test(href) ? ( + { + if (window.electronAPI?.openExternalUrl) { + event.preventDefault(); + void window.electronAPI.openExternalUrl(href); + } + }} + > + {children} + + ) : ( + {children} + ), + }} + > + {text} + +
+ ); +} diff --git a/src/components/ai-edition/LeftPanel.stop.test.tsx b/src/components/ai-edition/LeftPanel.stop.test.tsx index bce7ff467..36d0a7014 100644 --- a/src/components/ai-edition/LeftPanel.stop.test.tsx +++ b/src/components/ai-edition/LeftPanel.stop.test.tsx @@ -42,7 +42,7 @@ vi.mock("@/contexts/I18nContext", () => ({ useI18n: () => ({ locale: "en" }), useScopedT: () => (key: string) => key, })); -vi.mock("sonner", () => ({ toast: { error: api.error, warning: api.warning } })); +vi.mock("sonner", () => ({ toast: { error: api.error, warning: api.warning, success: vi.fn() } })); let onEvent: (event: AiEditionChatEvent) => void; let replies: ((value: AiEditionChatResult) => void)[]; const doc = (title: string, projectId = "p") => createEmptyDocument({ projectId, title }); @@ -303,4 +303,73 @@ describe("composer Stop", () => { await act(async () => replies[1]({ ...result("Disposed", true), document: doc("Disposed") })); expect(api.save).not.toHaveBeenCalled(); }); + it("keeps Markdown source on send/copy and renders streaming invocation updates without duplicates", async () => { + const copy = vi.fn().mockResolvedValue(undefined); + window.electronAPI.copyToClipboard = copy; + panel(); + await screen.findByText("s"); + const source = " indented\n\n```ts\n const x = 1;\n```\n"; + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: source } }); + fireEvent.keyDown(input, { key: "Enter", shiftKey: true }); + expect(api.run).not.toHaveBeenCalled(); + fireEvent.keyDown(input, { key: "Enter" }); + await waitFor(() => expect(api.run).toHaveBeenCalled()); + expect(api.run.mock.calls[0][2]).toBe(source); + const runId = api.run.mock.calls[0][4] as string; + const step = { + id: "first", + kind: "tool" as const, + name: "getTranscript", + arguments: "assetId: missing", + startedAt: 0, + status: "running" as const, + }; + act(() => + onEvent({ projectId: "p", sessionId: "s", runId: "old", kind: "executionStep", step }), + ); + expect(screen.queryByText(/getTranscript/)).toBeNull(); + act(() => onEvent({ projectId: "p", sessionId: "s", runId, kind: "executionStep", step })); + expect(screen.getAllByText("getTranscript")).toHaveLength(1); + const failed = { ...step, status: "failed" as const, durationMs: 15, summary: "No transcript" }; + act(() => + onEvent({ projectId: "p", sessionId: "s", runId, kind: "executionStep", step: failed }), + ); + expect(screen.getAllByText("getTranscript")).toHaveLength(1); + const response = result("## Finished\n\n- Preserved source"); + response.assistantMessage!.executionSteps = [failed]; + await act(async () => replies[0](response)); + expect(screen.getByRole("heading", { name: "Finished" })).toBeInTheDocument(); + expect(screen.getAllByText("getTranscript")).toHaveLength(1); + fireEvent.click(screen.getAllByRole("button", { name: "chat.copyMessage" })[0]); + expect(copy).toHaveBeenCalledWith(source); + }); + it("retains failure steps on the original user message", async () => { + panel(); + await start(); + const step = { + id: "failure", + kind: "tool" as const, + name: "getTranscript", + arguments: "", + startedAt: 0, + status: "failed" as const, + summary: "No transcript", + }; + await act(async () => + replies[0]({ + success: false, + userMessageId: "stored", + userMessage: { + id: "stored", + role: "user", + createdAt: "now", + content: "Edit", + executionSteps: [step], + }, + }), + ); + expect(screen.getByText("getTranscript")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toBeEnabled(); + }); }); diff --git a/src/components/ai-edition/LeftPanel.tsx b/src/components/ai-edition/LeftPanel.tsx index 7f24ca7d7..fc97c437b 100644 --- a/src/components/ai-edition/LeftPanel.tsx +++ b/src/components/ai-edition/LeftPanel.tsx @@ -6,6 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Tooltip } from "@/components/ui/tooltip"; import { useEditorDialogActions, useEditorDialogSection } from "@/contexts/EditorDialogsContext"; import { useScopedT } from "@/contexts/I18nContext"; +import { type ChatExecutionStep, updateExecutionStep } from "@/lib/ai-edition/chatExecution"; import { applyAgentDocumentIfCurrent, runAgentTurn, @@ -26,6 +27,8 @@ import { type ReasoningEffort, } from "../../../electron/ai-edition/provider-registry"; import { ChatUsageSummary } from "./ChatUsageSummary"; +import { ChatExecutionSteps } from "./ChatExecutionSteps"; +import { ChatMarkdown } from "./ChatMarkdown"; import { ChatWelcome } from "./ChatWelcome"; import { canSendChat } from "./chatAvailability"; import { adoptStoredUserMessage } from "./chatTranscript"; @@ -36,6 +39,7 @@ import { useChatBudget } from "./useChatBudget"; interface ChatDisplayMessage { usage?: AgentTurnUsage; durationMs?: number; + executionSteps?: ChatExecutionStep[]; stopped?: boolean; id?: string; role: string; @@ -413,7 +417,7 @@ function ThinkingBlock({ font: "400 12px/1.5 var(--font-body)", }} > - {text} + ) : (
([]); const latestRunIdRef = useRef(null); const runRef = useRef<{ id: string; @@ -582,6 +587,8 @@ export function ChatStripPanel() { setThinkingText(run.thinking); } else if (event.kind === "text") setLiveText((prev) => prev + event.delta); else if (event.kind === "usage") setLiveUsage(event.usage); + else if (event.kind === "executionStep") + setLiveSteps((previous) => updateExecutionStep(previous, event.step)); }); return () => { unsubscribe(); @@ -598,6 +605,7 @@ export function ChatStripPanel() { setThinkingText(""); setLiveText(""); setLiveUsage(undefined); + setLiveSteps([]); } }, [projectId, activeSessionId, abandonRun]); @@ -668,6 +676,7 @@ export function ChatStripPanel() { toolCalls: m.toolCalls, usage: m.usage, durationMs: m.durationMs, + executionSteps: m.executionSteps, checkpointId: m.checkpointId ?? null, stopped: m.stopped, })), @@ -689,8 +698,8 @@ export function ChatStripPanel() { }); const send = async (overrideText?: string) => { - const text = (overrideText ?? input).trim(); - if (!projectId || !text || busy || runRef.current) return; + const text = overrideText ?? input; + if (!projectId || !text.trim() || busy || runRef.current) return; // ponytail: nothing to talk to. Bounce to the settings modal instead of // firing a doomed request. The composer is disabled in this state too, // but Auto-enhance calls send() directly and Enter can slip through. @@ -718,6 +727,7 @@ export function ChatStripPanel() { setBusy(true); setStopping(false); setLiveText(""); + setLiveSteps([]); // ponytail: prepare the live reasoning-trace accumulator. Late events // from a previous run (or from another panel/window) won't match this // sessionId and are dropped by the subscription above. @@ -821,6 +831,7 @@ export function ChatStripPanel() { usage: assistant.usage, durationMs: assistant.durationMs, content: assistant.content, + executionSteps: assistant.executionSteps, stopped: assistant.stopped, time: new Date().toISOString(), toolCalls: assistant.toolCalls, @@ -844,6 +855,7 @@ export function ChatStripPanel() { checkpointId: user.checkpointId ?? null, usage: user.usage, durationMs: user.durationMs, + executionSteps: user.executionSteps, } : message, ), @@ -864,6 +876,7 @@ export function ChatStripPanel() { setThinkingText(""); setLiveText(""); setLiveUsage(undefined); + setLiveSteps([]); setThinkingExpanded(false); } else if ( !runRef.current && @@ -941,6 +954,7 @@ export function ChatStripPanel() { toolCalls: m.toolCalls, usage: m.usage, durationMs: m.durationMs, + executionSteps: m.executionSteps, checkpointId: m.checkpointId ?? null, stopped: m.stopped, })), @@ -1093,6 +1107,7 @@ export function ChatStripPanel() { content: m.content, time: m.createdAt, toolCalls: m.toolCalls, + executionSteps: m.executionSteps, stopped: m.stopped, usage: m.usage, durationMs: m.durationMs, @@ -1484,7 +1499,8 @@ export function ChatStripPanel() { ) : null}
- {m.thinking && m.role !== "user" ? ( + + {!m.executionSteps?.length && m.thinking && m.role !== "user" ? ( - {m.content} +
) : null} - {m.toolCalls?.length ? ( + {!m.executionSteps?.length && m.toolCalls?.length ? (
{m.toolCalls.map((call, j) => (
{t("chat.authorAssistant")}
- {liveText ?
{liveText}
: null} + + {liveText ? : null} {stopping ?
{t("chat.stopping")}
: null}
- {!thinkingText ? ( + {!thinkingText && !liveSteps.length ? ( {t("chat.thinking")} ) : null}
- {thinkingText ? ( + {thinkingText && !liveSteps.length ? ( ; +export type ToolExecutionStep = Extract; + +/** Update by invocation identity, retaining observed order even when calls finish out of order. */ +export function updateExecutionStep(steps: ChatExecutionStep[], step: ChatExecutionStep) { + const index = steps.findIndex((entry) => entry.id === step.id); + return index < 0 ? [...steps, step] : steps.map((entry, i) => (i === index ? step : entry)); +} + +/** A process restart cannot leave a persisted tool pretending to still run. */ +export function restoredExecutionSteps(steps?: ChatExecutionStep[]) { + return steps?.map( + (step): ChatExecutionStep => + step.kind === "tool" && step.status === "running" ? { ...step, status: "stopped" } : step, + ); +} diff --git a/src/native/contracts.ts b/src/native/contracts.ts index d75ce303b..9ee9ef7fd 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -1,3 +1,4 @@ +import type { ChatExecutionStep } from "../lib/ai-edition/chatExecution"; import type { StylePreset, StylePresetAppearance } from "../lib/ai-edition/stylePresets"; export const NATIVE_BRIDGE_CHANNEL = "native-bridge:invoke"; @@ -417,6 +418,7 @@ export interface AgentTurnUsage { } export interface AiEditionChatMessage { + executionSteps?: ChatExecutionStep[]; stopped?: boolean; /** Elapsed agent turn time, measured in the main process. Absent on older messages. */ durationMs?: number; @@ -435,6 +437,7 @@ export interface AiEditionChatMessage { } export interface AiEditionChatResult { + userMessage?: AiEditionChatMessage; cancelled?: boolean; userMessage?: AiEditionChatMessage; success: boolean; @@ -1071,6 +1074,7 @@ export type NativeBridgeEventName = // provider failed and the toast was a side-channel; the renderer shows it // inline so the chat doesn't appear to "echo back my question". export type AiEditionChatEvent = { projectId: string; runId: string } & ( + | { kind: "executionStep"; sessionId: string; step: ChatExecutionStep } | { kind: "text"; sessionId: string; delta: string } | { kind: "thinking"; sessionId: string; delta: string } | { kind: "usage"; sessionId: string; usage: AgentTurnUsage } diff --git a/technical-documentation/architecture/ai-agent.md b/technical-documentation/architecture/ai-agent.md index cf71959bb..4565da726 100644 --- a/technical-documentation/architecture/ai-agent.md +++ b/technical-documentation/architecture/ai-agent.md @@ -140,3 +140,14 @@ Document checkpoints stay in process memory. After a restart the conversation co - Cursor telemetry is read through an injected `CursorTelemetryReader` (`electron/ipc/handlers.ts` builds the only production one, behind `resolveApprovedVideoPath`). A runtime with no reader wired answers `reason: "unavailable"` on every call and reports `hasCursorTelemetry: null` — honest, and useless. There is no cache: a turn probes each asset's sidecar once, then reads it in full for every call in `TOOLS_READING_CURSOR` — `getCursorTrack` and the three zoom writes, which need it to report `cursorAnchor`. N separate `addZoom` calls therefore re-read the same file N times; `addZooms` is what keeps a whole zoom pass to one read. - `readCursorSidecar`'s normalizer flattens `double-click` / `right-click` / `middle-click` to `move` before the track is built, so those clicks reach the model as plain moves rather than as the events they were. The narrowing is upstream, in `CursorRecordingSample`. - The deep-agent instance is rebuilt for every turn without a LangGraph checkpointer, so stateful agent threads do not persist independently of the explicit chat history. + + +## Execution steps and Markdown + +Agent turns stream an ordered execution trace separately from the final answer. Each tool invocation has an independent ID, bounded argument summary, status, duration and result/error summary. Model-announced calls are reconciled with executor verdicts, including schema-invalid and unknown tools that fail before the document executor runs. Repeated calls remain separate; completion updates the existing row. Only provider-exposed reasoning is recorded, in separate collapsed blocks between tools. + +Execution metadata is stored with the originating chat turn in the existing project sidecar. Failed turns keep their trace on the user message; successful and stopped turns keep it on the assistant message. A session deletion or rewind invalidates a pending turn. Switching sessions follows Stop's cancellation behavior and does not move the trace into the new conversation. After restart, a persisted running tool is shown as stopped rather than still running. Legacy histories and invalid optional trace metadata remain readable. + +The sidecar stores bounded display metadata, not raw provider events or unrestricted tool results. Argument summaries contain IDs, counts and numeric/boolean options; arbitrary text, headers, paths and credentials are excluded. Result summaries use the executor's short summary/error or structural counts. Known credential patterns are redacted. Each tool summary is capped at 1,000 characters and each exposed reasoning block at 32,768 characters. + +User messages, live/final replies, reasoning and human-readable summaries render through `react-markdown` with GFM support. HTML is skipped, remote images render as alt text, and links accept only HTTP(S) or mailto through the existing desktop URL handler. Markdown source, including multiline indentation and code fences, remains unchanged for send, copy and persistence. Enter sends; Shift+Enter inserts a line break. diff --git a/technical-documentation/testing/manual-e2e-checklist.md b/technical-documentation/testing/manual-e2e-checklist.md index 26ab3d873..8b8ea5061 100644 --- a/technical-documentation/testing/manual-e2e-checklist.md +++ b/technical-documentation/testing/manual-e2e-checklist.md @@ -1074,3 +1074,4 @@ These checks are the human slice: an agent cannot click Apple's picker (see abov | 2026-11-04 | `main` @ `290bd3a` + `fix/export-sparse-clip-start`, locally built compositor addon and app renderer (2.2.1) | macOS 27.0.1, arm64, Metal | **Partial — automated export regression; GUI not covered** | Synthetic one-update-per-second H.264 reproduces #1479 on installed 2.2.1 and in the new native test before the fix. Patched frame inspection holds green at 1.25 s and 1.983 s, then shows blue at 2.017 s; MP4 frame counts pass with/without camera and after a preceding clip. Patched app CLI exports the synthetic project and the affected private project with all original cuts unchanged; private media is not attached. Native unit suite: 492 passed; export timing: 15 passed, one existing bounded-tail test fails identically on unmodified main (container duration clips the requested tail before seeking). JS: 5192 passed, 5 skipped; both TypeScript checks and lint pass (existing warnings). **Skipped:** real OS GUI export, because computer-use initialization and app selection both timed out; capture, devices, preview interactions, signing/packaging, Windows and Linux were not exercised. This is not a release-candidate pass. | | 2026-10-21 | PR #1177, rebased on `main` @ `1e917109`, token presentation follow-up (dev build, Electron 41.2.1) | macOS 15.8, Apple Silicon | **Partial: Agent usage presentation** | Real Electron driven through OS computer-use, isolated `/tmp` profile and a local scripted OpenAI-compatible provider (no paid provider). A two-call turn showed muted `≥10.3k tokens` beside the activity indicator, then `12.3k tokens · 15s` beneath the reply. Input/output/cache details remained in the tooltip. During a second turn, creating Conversation 2 hid the running indicator/count there; returning to Conversation 1 restored the running count and then exactly one completed reply. Fully quit/relaunched Electron and reopened the editor: both completed summaries and their durations restored in Conversation 1, with old rewind controls absent. Sidecar inspection confirmed Conversation 2 remained empty. Skipped: capture/preview/export, native binaries, paid-provider billing, Windows/Linux and the rest of this checklist; chat-only change, not release-promotion QA. | | 2026-10-22 | PR #1177, maintainer presentation follow-up on `54ee4e15` (dev build, Electron 41.2.1) | macOS 15.8, Apple Silicon | **Partial: Agent usage presentation** | Real Electron driven through OS computer-use with an isolated `/tmp` profile and a local scripted Anthropic-compatible provider (synthetic counts, no paid provider). A two-call turn showed muted `≥10.3k tokens` beside the spinner, one expandable Thinking label while reasoning streamed, then `12.3k tokens · 45s` beneath the reply. Captured [running](https://raw.githubusercontent.com/auberginewly/openscreen-community/2cf6e8435c9214981243088aebc470380e6f8611/technical-documentation/testing/screenshots/pr-1177/running.png) and [completed](https://raw.githubusercontent.com/auberginewly/openscreen-community/2cf6e8435c9214981243088aebc470380e6f8611/technical-documentation/testing/screenshots/pr-1177/completed.png) screenshots. Component tests also cover partial totals with details confined to the tooltip, unavailable usage showing duration only, and unavailable usage without duration rendering nothing. Skipped: capture/preview/export, native binaries, paid-provider billing, restart/session switching (covered in the preceding pass), Windows/Linux and the rest of this checklist; chat presentation only. | +| 2026-10-27 | Dev `feat/agent-execution-steps-1169` on `main` @ `5bb1b3fb`, Electron 41.2.1; Vite 5191, private `/tmp/openscreen-execution-ui-*` profile and local Anthropic-shaped fixture provider | macOS 15.8, Apple Silicon | **Partial — execution steps and Markdown** | Real Electron driven through OS computer-use. Sent a multiline Markdown request with a heading, list and indented code fence; observed two separate successful `getCurrentDocument` rows and one failed `getTranscript` row, with two provider-exposed reasoning blocks in order. Expanded reasoning and failure details. Completed answer rendered heading, list, code and GFM table. Corrected code-block contrast to use theme colors. New conversations remained empty; switching during the second turn cancelled it and saved a stopped assistant with five steps in its original session. Fully quit/relaunched, reopened Studio and verified both completed/stopped traces, failure labels and Markdown restored without usable old rewind controls. Final observer implementation was exercised in a third real SDK/HTTP turn with no duplicate rows. Automated tests additionally cover schema-invalid/unknown calls, out-of-order completions, failed-turn persistence, source copying and unsafe Markdown. Screenshots retained in the task evidence. Skipped: paid/remote providers, capture/audio/webcam, native preview/export, Windows/Linux runtime and the rest of this checklist; chat-only slice, not release-promotion QA. | From d8a647379029949a5ffede770a7bef973f24c10b Mon Sep 17 00:00:00 2001 From: wuzixuan06 Date: Tue, 27 Oct 2026 17:08:57 +0800 Subject: [PATCH 2/4] fix(nix): update npm dependency hash for chat Markdown --- nix/package.nix | 2 +- package-lock.json | 77 +++++++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 77fa7f1d8..2c0ea7ece 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -70,7 +70,7 @@ buildNpmPackage { ); }; - npmDepsHash = "sha256-kqvXt9uwinx/HQBkwbPG7PMCUtqMgeZKOYFsmwox3cw="; + npmDepsHash = "sha256-Z+TVNmWYxB+LgwQAf1FTFum/P6lOL0AV8DReAzLVa0o="; env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; diff --git a/package-lock.json b/package-lock.json index 50fec0675..6d209305f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8100,9 +8100,9 @@ } }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.7.tgz", + "integrity": "sha512-F7DNV2NMcTxA4GqPVHnXzX+mpoPHAIY9rJAVH3CZ1FdBZOcBUEjUuvMkMpHVs/sicEtznh9tMGj5WUR9S4E2rg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -8117,7 +8117,7 @@ "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", + "style-to-object": "^2.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" }, @@ -8357,9 +8357,9 @@ "license": "ISC" }, "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.9.tgz", + "integrity": "sha512-IttN8BqKLxzUrnTqHI+/hwAJQItBAUjdoHERCDv+oOuX48VP6bzW8+QjOXSfoevzq3YrYAwX1uLHp8TqO9yIeg==", "license": "MIT" }, "node_modules/ip-address": { @@ -9155,9 +9155,9 @@ } }, "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.1.0.tgz", + "integrity": "sha512-xpeSCdM+B+6P3ncfgaB7FoDbtJ30xAe6Z18TGT7AgXJsHqCElCICZovQqdRMm7kHgpTyYv36EupqewoFFSB8vw==", "license": "MIT", "funding": { "type": "github", @@ -9259,9 +9259,9 @@ } }, "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-Jx6QSK8wDo558Bcf7q+1pe0v81g2PVs09Hlvu9xJHmCFRF3sAPW7vXFVIAwBncs3sde92WbsVqNxwdqViA2XnQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9308,16 +9308,18 @@ } }, "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.1.tgz", + "integrity": "sha512-fiIjPrAy4kz68/eFbPHhsiWDGHhqUSVoU1XsfLM5DsEPXw7MxgiVVyulUggFRckVWeT1KXQXkg5vm3kv4oMSOA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" + "mdast-util-to-markdown": "^2.0.0", + "mdast-util-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" }, "funding": { "type": "opencollective", @@ -9325,9 +9327,9 @@ } }, "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.1.tgz", + "integrity": "sha512-jXVO1hvB2hs82g1esi7I9nIJDqiijbQLIyH2jDUb5s7jIAZMr/79h0VHSq7XHKTqpLARRm+DDNjduB7S9+6XZA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -9359,9 +9361,9 @@ } }, "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.1.tgz", + "integrity": "sha512-oXp1GIa+E9IwIs3a106+tr3Bz2MlTTBvlXjOZMoIat11kk7vjqgmBnksB9WHmWv+gZh7k3IUgpAHlwHB7DH4GQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -9436,9 +9438,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.1.tgz", - "integrity": "sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.3.tgz", + "integrity": "sha512-dImdB6VZRMRac1SwAW6o1c0ESk/7pFFAMCMuKM+9v7LpTSthMM5nzXpqKw4Mjo66aXVKhNkyrP4bMB9MurPyWg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12689,22 +12691,13 @@ "node": ">=8" } }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-2.0.2.tgz", + "integrity": "sha512-QmbXPUylqU80HAgTOJeEOdbA40W9KtxmjMhQy+tq+b8F2aoh3aQLuy82hh2jm2qsC9hR9eI0+eQidTMydKcqRA==", "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.7" + "inline-style-parser": "0.2.9" } }, "node_modules/sucrase": { @@ -13573,9 +13566,9 @@ } }, "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.4.tgz", + "integrity": "sha512-66ycRswIkNmHH9PHku7ADtv5RVwgIrOcuv3J8Svz6h4qgGRrKVa+9/neuKyJZUXyOpYwLTvS+whZ4R2urHHgQg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", From bdd2d3c0ab6fd8026edc568e080d1b71198c8cdb Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sat, 7 Nov 2026 22:45:24 +0100 Subject: [PATCH 3/4] fix(ai): persist a tool start immediately, debounce its later updates --- electron/ai-edition/chat-service.execution.test.ts | 14 ++++++++++++++ electron/ai-edition/chat-service.ts | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/electron/ai-edition/chat-service.execution.test.ts b/electron/ai-edition/chat-service.execution.test.ts index 34439505e..b92898638 100644 --- a/electron/ai-edition/chat-service.execution.test.ts +++ b/electron/ai-edition/chat-service.execution.test.ts @@ -96,6 +96,20 @@ it.each([ expect(persisted).not.toContain("resultJson"); }); +it("writes a tool start to disk before the turn ends", async () => { + const session = createSession("p"); + let onDisk = ""; + vi.mocked(invokeOpenScreenAgent).mockImplementationOnce(async (args) => { + args.sink.executionStep?.(toolExecutionStep("getTranscript", {})); + await flushChatPersistence(); + onDisk = await readFile(join(root, "p.chat.json"), "utf8"); + return { text: "Done", document: args.document, mutated: false }; + }); + await runChat("p", session.id, "hello", config); + expect(onDisk).toContain('"getTranscript"'); + expect(onDisk).toContain('"running"'); +}); + it("does not recreate execution history after the originating session is deleted", async () => { const session = createSession("p"); vi.mocked(invokeOpenScreenAgent).mockImplementationOnce(async (args) => { diff --git a/electron/ai-edition/chat-service.ts b/electron/ai-edition/chat-service.ts index a367e2695..2247d97c2 100644 --- a/electron/ai-edition/chat-service.ts +++ b/electron/ai-edition/chat-service.ts @@ -453,7 +453,8 @@ export async function runChat( if (!isOriginValid()) return; userMessage.executionSteps = execution.snapshot(); emit.executionStep(step); - if (step.kind === "tool") persistProject(projectId); + // A start is written at once: restart recovery can only stop a step it can read. + if (step.kind === "tool" && step.status === "running") persistProject(projectId); else if (!pendingSave) pendingSave = setTimeout(() => { pendingSave = undefined; From 99146706ded4873686071a8a4a4ba43761c95429 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sat, 7 Nov 2026 22:50:29 +0100 Subject: [PATCH 4/4] fix(ai): reconcile execution steps with token usage after the rebase --- electron/ai-edition/chat-service.execution.test.ts | 13 +++++++++++-- electron/ai-edition/chat-service.ts | 2 -- src/components/ai-edition/LeftPanel.tsx | 2 +- src/native/contracts.ts | 1 - 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/electron/ai-edition/chat-service.execution.test.ts b/electron/ai-edition/chat-service.execution.test.ts index b92898638..4767b2ae7 100644 --- a/electron/ai-edition/chat-service.execution.test.ts +++ b/electron/ai-edition/chat-service.execution.test.ts @@ -20,6 +20,14 @@ import { invokeOpenScreenAgent } from "./deep-agent/service"; import type { LlmConfigStore } from "./llm-config-store"; let root: string; +const usage = { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + modelCallCount: 0, + countedCallCount: 0, + status: "unavailable" as const, +}; const config = { getConfig: () => ({ provider: "openai", model: "fixture" }), getCredential: () => ({ value: "test" }), @@ -63,6 +71,7 @@ it.each([ document: args.document, mutated: false, cancelled: mode === "stopped", + usage, }; }); const source = " indented\n\n```ts\n const x = 1;\n```\n"; @@ -103,7 +112,7 @@ it("writes a tool start to disk before the turn ends", async () => { args.sink.executionStep?.(toolExecutionStep("getTranscript", {})); await flushChatPersistence(); onDisk = await readFile(join(root, "p.chat.json"), "utf8"); - return { text: "Done", document: args.document, mutated: false }; + return { text: "Done", document: args.document, mutated: false, usage }; }); await runChat("p", session.id, "hello", config); expect(onDisk).toContain('"getTranscript"'); @@ -116,7 +125,7 @@ it("does not recreate execution history after the originating session is deleted args.sink.thinking("Inspect"); deleteSession("p", session.id); args.sink.executionStep?.(toolExecutionStep("getTranscript", {})); - return { text: "Done", document: args.document, mutated: false }; + return { text: "Done", document: args.document, mutated: false, usage }; }); const result = await runChat("p", session.id, "hello", config); expect(result.success).toBe(false); diff --git a/electron/ai-edition/chat-service.ts b/electron/ai-edition/chat-service.ts index 2247d97c2..be9b31d00 100644 --- a/electron/ai-edition/chat-service.ts +++ b/electron/ai-edition/chat-service.ts @@ -544,7 +544,6 @@ export async function runChat( } if (!result.text && !result.cancelled) { - persistProject(projectId); // ponytail: surface the deep-agent's diagnostic so the user can see // *why* the model produced no text (e.g. MiniMax streaming only // thinking blocks, or a non-text content shape that our extractor @@ -554,7 +553,6 @@ export async function runChat( originatingMessage.durationMs = durationMs; persistProject(projectId); return { - userMessage, success: false, error: result.reason ?? "Empty response from model.", // The message is stored all the same, and can be rewound to retry it. diff --git a/src/components/ai-edition/LeftPanel.tsx b/src/components/ai-edition/LeftPanel.tsx index fc97c437b..d31a28514 100644 --- a/src/components/ai-edition/LeftPanel.tsx +++ b/src/components/ai-edition/LeftPanel.tsx @@ -26,9 +26,9 @@ import { PROVIDER_DEFINITIONS, type ReasoningEffort, } from "../../../electron/ai-edition/provider-registry"; -import { ChatUsageSummary } from "./ChatUsageSummary"; import { ChatExecutionSteps } from "./ChatExecutionSteps"; import { ChatMarkdown } from "./ChatMarkdown"; +import { ChatUsageSummary } from "./ChatUsageSummary"; import { ChatWelcome } from "./ChatWelcome"; import { canSendChat } from "./chatAvailability"; import { adoptStoredUserMessage } from "./chatTranscript"; diff --git a/src/native/contracts.ts b/src/native/contracts.ts index 9ee9ef7fd..e7828e939 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -437,7 +437,6 @@ export interface AiEditionChatMessage { } export interface AiEditionChatResult { - userMessage?: AiEditionChatMessage; cancelled?: boolean; userMessage?: AiEditionChatMessage; success: boolean;