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
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
* project slug walked back onto the filesystem) and no tokens, an unchanged
|
||||
* file never re-read, a quiet one settled to completed, a session the user
|
||||
* deleted staying deleted, and a non-cursor row with the same id never
|
||||
* touched. The Cursor dir and the DB are temp dirs; the transcripts are
|
||||
* synthetic, in the shape Cursor writes.
|
||||
* touched, a session claimed by the Cursor pane it started in, and the
|
||||
* transcript mapped to the Claude transcript shape by the
|
||||
* transcript routes. The Cursor dir and the DB are temp dirs; the transcripts
|
||||
* are synthetic, in the shape Cursor writes.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -16,13 +18,18 @@ const path = require("node:path");
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-cursor-usage-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "dashboard.db");
|
||||
process.env.AGENTHUB_CURSOR_DIR = path.join(ROOT, "cursor");
|
||||
process.env.CLAUDE_HOME = path.join(ROOT, "claude");
|
||||
|
||||
const { describe, it, beforeEach, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { db } = require("../db");
|
||||
const cursor = require("../lib/cursor-usage");
|
||||
|
||||
after(() => fs.rmSync(ROOT, { recursive: true, force: true }));
|
||||
let server;
|
||||
after(() => {
|
||||
server?.close();
|
||||
fs.rmSync(ROOT, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const ID = "7481e37e-9157-4145-b985-2ea9a2117f52";
|
||||
const PROJECT = path.join(fs.realpathSync(ROOT), "work", "my app");
|
||||
@@ -65,7 +72,8 @@ const session = (id) => db.prepare("SELECT * FROM sessions WHERE id = ?").get(id
|
||||
beforeEach(() => {
|
||||
fs.rmSync(process.env.AGENTHUB_CURSOR_DIR, { recursive: true, force: true });
|
||||
db.exec(
|
||||
"DELETE FROM token_usage; DELETE FROM agents; DELETE FROM sessions; DELETE FROM cursor_transcripts;"
|
||||
"DELETE FROM token_usage; DELETE FROM agents; DELETE FROM sessions; DELETE FROM cursor_transcripts;" +
|
||||
" DELETE FROM dashboard_runs; DELETE FROM lanes;"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -175,4 +183,64 @@ describe("cursor usage", () => {
|
||||
assert.equal(session(ID).name, `Cursor ${ID.slice(0, 8)}`);
|
||||
assert.equal(session(ID).cwd, null);
|
||||
});
|
||||
|
||||
it("maps the transcript to the transcript shape", async () => {
|
||||
const file = transcript(ID);
|
||||
const more = [
|
||||
{
|
||||
role: "assistant",
|
||||
message: {
|
||||
content: [
|
||||
{ type: "text", text: "Reading it now." },
|
||||
{ type: "tool_use", id: "t1", name: "Grep", input: { pattern: "x" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
message: { content: [{ type: "tool_result", tool_use_id: "t1", content: "no match" }] },
|
||||
},
|
||||
{ role: "assistant", message: { content: [{ type: "text", text: "Done." }] } },
|
||||
];
|
||||
fs.appendFileSync(file, more.map((l) => JSON.stringify(l)).join("\n") + "\n");
|
||||
await cursor.ingestCursorUsage();
|
||||
assert.equal(cursor.transcriptPath(ID), file);
|
||||
|
||||
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", ["tool_use"]],
|
||||
["assistant", ["text", "tool_use"]],
|
||||
["tool", ["tool_result"]],
|
||||
["assistant", ["text"]],
|
||||
]
|
||||
);
|
||||
assert.equal(body.messages[0].content[0].text, "Review the campaign code\nplease");
|
||||
assert.equal(body.messages[3].content[0].output, "no match");
|
||||
const sub = await (await fetch(`${BASE}/api/sessions/${ID}/transcript?agent_id=x`)).json();
|
||||
assert.equal(sub.messages.length, 0);
|
||||
});
|
||||
|
||||
it("gives the session to the Cursor pane it started in", async () => {
|
||||
db.prepare("INSERT INTO lanes (id, title, cwd) VALUES (5, 'w', ?)").run(PROJECT);
|
||||
db.prepare(
|
||||
`INSERT INTO dashboard_runs (id, lane_id, cwd, status, started_at, tool)
|
||||
VALUES ('agenthub-lane-5', 5, ?, 'running', ?, 'cursor')`
|
||||
).run(PROJECT, new Date(Date.now() - 60_000).toISOString());
|
||||
transcript(ID);
|
||||
await cursor.ingestCursorUsage();
|
||||
assert.equal(session(ID).lane_id, 5);
|
||||
const run = db.prepare("SELECT session_id FROM dashboard_runs WHERE id = 'agenthub-lane-5'");
|
||||
assert.equal(run.get().session_id, ID);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user