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
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
/**
|
|
* @file ConversationView.pushed.test.tsx
|
|
* @description The server says a session's transcript grew
|
|
* (`transcript_changed`): the chat showing that session reads the new lines
|
|
* at once, and again within a short window after a burst — not at the hook
|
|
* throttle's 1.5 s pace — while another session's notice fetches nothing.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { act, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { ConversationView } from "../ConversationView";
|
|
import { api } from "../../../lib/api";
|
|
import { eventBus } from "../../../lib/eventBus";
|
|
|
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
|
const actual = await importOriginal<Record<string, unknown>>();
|
|
return {
|
|
...actual,
|
|
api: {
|
|
sessions: {
|
|
transcripts: vi.fn(async () => ({ transcripts: [] })),
|
|
transcript: vi.fn(async () => ({
|
|
messages: [
|
|
{
|
|
type: "user",
|
|
content: [{ type: "text", text: "hello" }],
|
|
timestamp: "2026-10-21T00:00:00Z",
|
|
},
|
|
],
|
|
total: 1,
|
|
has_more: false,
|
|
first_line: 1,
|
|
last_line: 1,
|
|
})),
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
const pushed = (session_id: string) =>
|
|
act(() => eventBus.publish({ type: "transcript_changed", data: { session_id } } as never));
|
|
const calls = () => vi.mocked(api.sessions.transcript).mock.calls.length;
|
|
|
|
describe("ConversationView on transcript_changed", () => {
|
|
it("reads its own session's new lines at once, and a burst within 300 ms", async () => {
|
|
render(<ConversationView sessionId="s1" fill live />);
|
|
await screen.findByText("hello");
|
|
vi.useFakeTimers();
|
|
const before = calls();
|
|
pushed("s1");
|
|
expect(calls()).toBe(before + 1);
|
|
pushed("s1");
|
|
pushed("s1");
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(350);
|
|
});
|
|
expect(calls()).toBe(before + 2);
|
|
});
|
|
|
|
it("ignores another session's transcript", async () => {
|
|
render(<ConversationView sessionId="s1" fill live />);
|
|
await screen.findByText("hello");
|
|
const before = calls();
|
|
pushed("s2");
|
|
await act(async () => {});
|
|
expect(calls()).toBe(before);
|
|
});
|
|
});
|