Files
AgentHub/server/__tests__/antigravity-transcript.test.js
T
nntrivi2001 383b9ae231 feat(chat): terminal chat parity for every tool; Desktop-like artifacts
- 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
2026-10-21 15:44:17 +07:00

147 lines
5.0 KiB
JavaScript

/**
* @file antigravity-transcript.test.js
* @description An Antigravity (agy) session's conversation view: its
* `transcript_full.jsonl` step log mapped to the Claude transcript shape —
* the prompt out of `<USER_REQUEST>`, the model's thinking, text, tool calls
* and tokens, each tool result tied to its call by step and an invalid call
* marked an error — one "main" transcript, no subagent files, and a session
* id that is no UUID finding nothing. The agy dir and the DB are temp dirs;
* the steps are synthetic, in the shape agy 1.4 writes.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const os = require("node:os");
const fs = require("node:fs");
const path = require("node:path");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-agy-transcript-"));
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db");
process.env.AGENTHUB_GEMINI_DIR = path.join(ROOT, "gemini");
process.env.CLAUDE_HOME = path.join(ROOT, "claude");
const { describe, it, after } = require("node:test");
const assert = require("node:assert/strict");
const { db } = require("../db");
const agy = require("../lib/antigravity-usage");
let server;
after(() => {
server?.close();
fs.rmSync(ROOT, { recursive: true, force: true });
});
const ID = "57154e1a-65e5-4b51-a116-1890f5ca831f";
const AT = "2026-10-21T05:08:24Z";
function steps(id) {
const dir = path.join(
process.env.AGENTHUB_GEMINI_DIR,
"antigravity-cli",
"brain",
id,
".system_generated",
"logs"
);
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, "transcript_full.jsonl");
const lines = [
{
step_index: 0,
source: "USER_EXPLICIT",
type: "USER_INPUT",
status: "DONE",
created_at: AT,
content:
"<USER_REQUEST>\nCreate c.txt\n</USER_REQUEST>\n<ADDITIONAL_METADATA>\nThe current local time is: x.\n</ADDITIONAL_METADATA>",
},
{
step_index: 1,
source: "MODEL",
type: "PLANNER_RESPONSE",
status: "DONE",
created_at: AT,
input_tokens: 3889,
cache_read_tokens: 768,
output_tokens: 12,
thinking: "Two commands.",
content: "Running them now.",
tool_calls: [
{ name: "run_command", args: { CommandLine: "echo ok > c.txt" } },
{ name: "read", args: { path: "c.txt" } },
],
},
{
step_index: 2,
source: "MODEL",
type: "GENERIC",
status: "DONE",
created_at: AT,
content: "Created At: x\nCompleted At: y\n\nThe command exited with code 0.\n",
},
{
step_index: 3,
source: "MODEL",
type: "GENERIC",
status: "INVALID",
error: 'unknown tool: "read"',
created_at: AT,
content: 'Created At: x\nCompleted At: y\nError invalid tool call: unknown tool: "read"',
},
{
step_index: 4,
source: "MODEL",
type: "PLANNER_RESPONSE",
status: "DONE",
created_at: AT,
content: "Done.",
},
];
fs.writeFileSync(file, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
return file;
}
describe("an Antigravity session's conversation", () => {
it("reads agy's step log in the Claude transcript shape", async () => {
const file = steps(ID);
assert.equal(agy.transcriptPath(ID), file);
assert.equal(agy.transcriptPath("../../etc"), null);
db.prepare(
"INSERT INTO sessions (id, name, status, started_at, tool) VALUES (?, 'c', 'completed', ?, 'antigravity')"
).run(ID, AT);
const { createApp, startServer } = require("../index");
server ||= await startServer(createApp(), 0);
const BASE = `http://127.0.0.1:${server.address().port}`;
const list = await (await fetch(`${BASE}/api/sessions/${ID}/transcripts`)).json();
assert.deepEqual(
list.transcripts.map((t) => [t.id, t.type]),
[["main", "main"]]
);
const body = await (await fetch(`${BASE}/api/sessions/${ID}/transcript`)).json();
assert.deepEqual(
body.messages.map((m) => [m.sender, m.content.map((c) => c.type)]),
[
["user", ["text"]],
["assistant", ["thinking", "text", "tool_use", "tool_use"]],
["tool", ["tool_result"]],
["tool", ["tool_result"]],
["assistant", ["text"]],
]
);
const [user, plan, ok, bad] = body.messages;
assert.equal(user.content[0].text, "Create c.txt");
assert.equal(user.timestamp, AT);
// Each result names the call it answers: call i of step s lands at step s+1+i.
assert.deepEqual(
plan.content.filter((c) => c.type === "tool_use").map((c) => c.id),
[ok.content[0].id, bad.content[0].id]
);
assert.equal(ok.content[0].output, "The command exited with code 0.\n");
assert.equal(ok.content[0].is_error, false);
assert.equal(bad.content[0].is_error, true);
assert.equal(plan.usage.input_tokens, 3889);
assert.equal(plan.usage.cache_read_input_tokens, 768);
const sub = await (await fetch(`${BASE}/api/sessions/${ID}/transcript?agent_id=x`)).json();
assert.equal(sub.messages.length, 0);
});
});