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
147 lines
5.0 KiB
JavaScript
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);
|
|
});
|
|
});
|