383b9ae231
- The chat over a terminal pane now covers Claude, Codex, omp, Cursor and Antigravity: Cursor's agent transcript and agy's step log (brain/<id>/.system_generated/logs/transcript_full.jsonl) are read as conversations; Cursor sessions are claimed by their workspace - Replies appear as soon as they are written: the projects-tree watcher broadcasts `transcript_changed` (at most every 250 ms per session) and the chat re-reads the transcript while the terminal underneath draws - Stop shows from the pane's own busy line (Claude/Codex `esc to interrupt`, agy `… esc to cancel`), or fresh output for omp/Cursor — no hooks needed - Permission prompts read off the terminal become a card with the tool, its command and "Deny with a note"; Codex gets Permission mode / Model buttons that open its own numbered pickers, answered from the menu card - Artifacts: cards for pages/documents/diagrams/tables/React components, a Preview | Code panel with versions, copy, download, save to the Artifacts Hub, full screen; sandboxed iframes without allow-same-origin - Claude panes get CLAUDE_CODE_ENABLE_TODO_TOOLS=1 so the Progress panel has a plan; multi-question pane menus read their Next row and question tabs - The realtime chat bridge (structured stdio, /api/chat-bridge, /ws-chat) from the unreleased ad9d475 is not kept
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
/**
|
|
* @file artifactView.test.ts
|
|
* @description What counts as an artifact and how it is shown: kinds by
|
|
* name, a React file only when written whole, a file's versions rebuilt from
|
|
* the conversation's Write/Edit/MultiEdit inputs (a failed call skipped, an
|
|
* edit to unseen text unknown), CSV rows, and the sandboxed documents.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
artifactDocument,
|
|
artifactKind,
|
|
artifactVersions,
|
|
frameSandbox,
|
|
isArtifact,
|
|
parseCsv,
|
|
} from "../artifactView";
|
|
import type { TranscriptMessage } from "../types";
|
|
|
|
const use = (id: string, name: string, input: Record<string, unknown>): TranscriptMessage => ({
|
|
type: "assistant",
|
|
timestamp: `2026-10-21T00:00:0${id}.000Z`,
|
|
content: [{ type: "tool_use", id, name, input } as TranscriptMessage["content"][number]],
|
|
});
|
|
const result = (id: string, isError = false): TranscriptMessage => ({
|
|
type: "user",
|
|
sender: "tool",
|
|
timestamp: null,
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
id,
|
|
output: "",
|
|
is_error: isError,
|
|
} as TranscriptMessage["content"][number],
|
|
],
|
|
});
|
|
|
|
describe("artifactView", () => {
|
|
it("knows an artifact by its name", () => {
|
|
expect(artifactKind("/a/page.HTML")).toBe("html");
|
|
expect(artifactKind("d.mmd")).toBe("mermaid");
|
|
expect(artifactKind("App.tsx")).toBe("react");
|
|
expect(artifactKind("x.go")).toBe("code");
|
|
expect(isArtifact("report.md", false)).toBe(true);
|
|
expect(isArtifact("App.tsx", false)).toBe(false);
|
|
expect(isArtifact("App.tsx", true)).toBe(true);
|
|
expect(isArtifact("server.js", true)).toBe(false);
|
|
});
|
|
|
|
it("rebuilds a file's versions from Write, Edit and MultiEdit", () => {
|
|
const msgs = [
|
|
use("1", "Write", { file_path: "/w/a.md", content: "# A\nold line\n" }),
|
|
result("1"),
|
|
use("2", "Edit", { file_path: "/w/a.md", old_string: "old", new_string: "new" }),
|
|
result("2"),
|
|
use("3", "Edit", { file_path: "/w/a.md", old_string: "missing", new_string: "x" }),
|
|
result("3", true),
|
|
use("4", "MultiEdit", {
|
|
file_path: "/w/a.md",
|
|
edits: [{ old_string: "# A", new_string: "# B" }],
|
|
}),
|
|
result("4"),
|
|
use("5", "Write", { file_path: "/w/other.md", content: "no" }),
|
|
];
|
|
const v = artifactVersions(msgs, "/w/a.md");
|
|
expect(v.map((x) => x.content)).toEqual([
|
|
"# A\nold line\n",
|
|
"# A\nnew line\n",
|
|
"# B\nnew line\n",
|
|
]);
|
|
});
|
|
|
|
it("says an edit to text it never saw is of unknown content", () => {
|
|
const v = artifactVersions(
|
|
[use("1", "Edit", { file_path: "a.md", old_string: "a", new_string: "b" })],
|
|
"a.md"
|
|
);
|
|
expect(v).toHaveLength(1);
|
|
expect(v[0]!.content).toBeNull();
|
|
});
|
|
|
|
it("reads CSV with quotes and newlines inside them", () => {
|
|
expect(parseCsv('a,b\n"x, y","say ""hi""\nthere"\n')).toEqual([
|
|
["a", "b"],
|
|
["x, y", 'say "hi"\nthere'],
|
|
]);
|
|
});
|
|
|
|
it("builds sandboxed documents that keep the source inert inside a script", () => {
|
|
const doc = artifactDocument("react", "export default () => <p>a</script>b</p>", false)!;
|
|
expect(doc).toContain("babel");
|
|
expect(doc).not.toContain("a</script>b");
|
|
expect(artifactDocument("mermaid", "graph TD; A-->B<img>", true)).toContain(
|
|
"A-->B<img>"
|
|
);
|
|
expect(artifactDocument("code", "x", false)).toBeNull();
|
|
expect(frameSandbox("svg")).toBe("");
|
|
expect(frameSandbox("html")).not.toContain("allow-same-origin");
|
|
});
|
|
});
|