| 1 | import { useSyncExternalStore } from "react"; |
| 2 | import { createRoot } from "react-dom/client"; |
| 3 | import { Transcript } from "../src/components/Transcript"; |
| 4 | import { Composer } from "../src/components/Composer"; |
| 5 | import { LocaleProvider } from "../src/lib/i18n"; |
| 6 | import { ToastProvider } from "../src/lib/toast"; |
| 7 | import { initialState, reducer, createTurnSubmissionId, type Action } from "../src/lib/useController"; |
| 8 | import { orderedLocalSubmissions } from "../src/lib/localSubmissionState"; |
| 9 | import { getTranscriptStore } from "../src/lib/transcriptStore"; |
| 10 | import { TranscriptSessionFollower } from "../src/lib/transcriptSessionFollower"; |
| 11 | import { installDesktopHostStub } from "../src/__tests__/desktopHostStub"; |
| 12 | import { setFrontendDiagnosticSink } from "../src/lib/frontendDiagnosticBridge"; |
| 13 | import type { FollowRequest, HistoryWindowRequest, Message, TranscriptFollowResponse } from "../src/generated/desktopContract.generated"; |
| 14 | import "../src/styles.css"; |
| 15 | |
| 16 | const tab = "handoff-integration", path = "/disposable/handoff.jsonl", store = getTranscriptStore(); |
| 17 | let state = { ...initialState }, revision = 1, coverage = 1; |
| 18 | const database: Message[] = Array.from({ length: 1000 }, (_, index) => [ |
| 19 | { role: "user", messageId: `user-${index}`, turnId: `turn-${index}`, content: `Question ${index}`, historyTurn: index + 1 }, |
| 20 | { role: "assistant", messageId: `answer-${index}`, turnId: `turn-${index}`, content: `Answer ${index}. Stable reading paragraph.\n\nSecond paragraph.`, historyTurn: index + 1, turnFinal: true }, |
| 21 | ]).flat(); |
| 22 | let polls: Array<(response: TranscriptFollowResponse) => void> = []; |
| 23 | let queued: NonNullable<TranscriptFollowResponse["changes"]> = []; |
| 24 | let pending: { id: string; text: string; messageId: string; turnId: string } | undefined; |
| 25 | let presentation: Record<string, unknown> = {}; |
| 26 | setFrontendDiagnosticSink((source, type, fields) => { if (source === "transcript" && type === "presentation") presentation = fields; }); |
| 27 | const dispatch = (action: Action) => { state = reducer(state, action); store.setState(tab, state); }; |
| 28 | function windowPage(request: HistoryWindowRequest) { |
| 29 | const limit = request.limit ?? 32; |
| 30 | let start = request.anchor === "newest" ? Math.max(0, database.length - limit) |
| 31 | : request.anchor === "message" ? Math.max(0, database.findIndex(item => item.messageId === request.messageId)) |
| 32 | : Number(request.cursor); |
| 33 | if (request.anchor === "cursor" && request.direction === "older") start = Math.max(0, start - limit); |
| 34 | const end = Math.min(database.length, start + limit); |
| 35 | return { status: "ready", generation: "fixture", snapshotSequence: coverage, coverageSequence: coverage, |
| 36 | totalTurns: 1000 + (database.length > 2000 ? 1 : 0), hasOlder: start > 0, hasNewer: end < database.length, |
| 37 | olderCursor: start > 0 ? String(start) : "", newerCursor: end < database.length ? String(end) : "", |
| 38 | messages: database.slice(start, end).map((message, offset) => ({ messageId: message.messageId!, role: message.role, |
| 39 | position: start + offset, version: 1, eventSequence: coverage, visibleTurn: message.historyTurn, submissionId: message.submissionId, inline: message })) }; |
| 40 | } |
| 41 | function publish(change: Omit<NonNullable<TranscriptFollowResponse["changes"]>[number], "revision" | "index" | "durableSeq" | "commitSeq">, commit = false) { |
| 42 | if (commit) coverage++; |
| 43 | queued.push({ ...change, revision: ++revision, index: 0, durableSeq: coverage, commitSeq: coverage, ...(commit ? { firstSeq: coverage } : {}) }); |
| 44 | flush(); |
| 45 | } |
| 46 | function flush() { |
| 47 | if (!polls.length || !queued.length) return; |
| 48 | polls.shift()!({ protocolVersion: 2, subscription: tab, changes: queued.splice(0), resetRequired: false }); |
| 49 | } |
| 50 | installDesktopHostStub({ |
| 51 | Commands: () => [], |
| 52 | ModelsForTab: () => [], |
| 53 | Models: () => [], |
| 54 | TranscriptFollowForTab: (_tab: string, request: FollowRequest) => { |
| 55 | if (request.close) return { protocolVersion: 2, subscription: tab, changes: [], resetRequired: false }; |
| 56 | if (!request.subscription) return { protocolVersion: 2, subscription: tab, changes: [], resetRequired: false, history: windowPage({ anchor: "newest" }), snapshot: { |
| 57 | protocolVersion: 1, snapshotId: "cut", identity: { sessionId: tab, runtimeEpoch: "fixture", rewriteEpoch: 0, headId: "" }, |
| 58 | projectionRevision: revision, coveredThroughSeq: coverage, durableSeq: coverage, records: [], activeRecords: [], activeAttempts: [], |
| 59 | runtime: { status: "completed", pendingEvents: [], samplingCount: 0, toolCount: 0 }, before: 0, hasOlder: true, totalRecords: database.length, totalTurns: 1000, stale: false, |
| 60 | } }; |
| 61 | return new Promise<TranscriptFollowResponse>(resolve => { polls.push(resolve); flush(); }); |
| 62 | }, |
| 63 | SessionHistoryWindowForTab: (_tab: string, request: HistoryWindowRequest) => windowPage(request), |
| 64 | TranscriptOutlineForTab: () => ({ turns: [], hasMore: false }), |
| 65 | }); |
| 66 | const follower = new TranscriptSessionFollower(tab, path, false, dispatch, () => state); |
| 67 | await follower.start(); |
| 68 | |
| 69 | async function page(direction: "older" | "newer" | "latest") { |
| 70 | const loaded = direction === "older" ? await store.loadOlder(tab, path) |
| 71 | : direction === "newer" ? await store.loadNewer(tab, path) : await store.loadLatest(tab, path); |
| 72 | if (!loaded) return; |
| 73 | dispatch({ type: "history_replace", ...loaded }); |
| 74 | } |
| 75 | function send(text: string) { |
| 76 | const id = createTurnSubmissionId(tab, state.sessionGen, state.seq); |
| 77 | pending = { id, text, messageId: `sent-${state.seq}`, turnId: `sent-turn-${state.seq}` }; |
| 78 | dispatch({ type: "user", text, submissionId: id, seq: state.seq }); |
| 79 | } |
| 80 | function event() { |
| 81 | if (!pending) return; |
| 82 | publish({ event: { kind: "user_message", source: "executor", messageId: pending.messageId, submissionId: pending.id, turnId: pending.turnId } }); |
| 83 | } |
| 84 | function record(withSubmission = true) { |
| 85 | if (!pending) return; |
| 86 | const message: Message = { role: "user", messageId: pending.messageId, turnId: pending.turnId, submissionId: withSubmission ? pending.id : undefined, content: pending.text, historyTurn: 1001 }; |
| 87 | if (!database.some(item => item.messageId === message.messageId)) database.push(message); |
| 88 | publish({ records: [message] }, true); |
| 89 | } |
| 90 | declare global { interface Window { handoff: { page: typeof page; event: typeof event; record: typeof record; batched(): void; send: typeof send; inspect(): unknown; finish(): void; output(): void } } } |
| 91 | window.handoff = { page, event, record, send, |
| 92 | batched() { event(); record(false); }, |
| 93 | output() { if (pending) publish({ event: { kind: "text", messageId: `output-${pending.messageId}`, turnId: pending.turnId, text: "Live answer with stable text." } }); }, |
| 94 | finish() { |
| 95 | if (pending) { |
| 96 | dispatch({ type: "send_confirmed", submissionId: pending.id }); |
| 97 | publish({ event: { kind: "message", messageId: `output-${pending.messageId}`, turnId: pending.turnId, |
| 98 | text: "Live answer with stable text.", reasoning: "Deterministic process details." } }); |
| 99 | } |
| 100 | publish({ runtime: { status: "completed", submissionId: pending?.id, pendingEvents: [], samplingCount: 0, toolCount: 0 } }); |
| 101 | }, |
| 102 | inspect() { return { ids: state.items.map(item => item.id), locals: state.localSubmissionOrder.length, handoffs: Object.keys(state.visibleSubmissionHandoffs).length, |
| 103 | users: state.items.filter(item => item.kind === "user").length, stats: store.stats(), presentation, pending, hasNewer: state.historyHasNewer, revision: state.localSubmissionSendRevision }; }, |
| 104 | }; |
| 105 | function Fixture() { |
| 106 | const current = useSyncExternalStore(listener => store.subscribeState(tab, listener), () => store.states.get(tab)!); |
| 107 | return <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}> |
| 108 | <div><button onClick={() => void page("older")}>Older</button><button onClick={() => void page("newer")}>Newer</button><button onClick={() => void page("latest")}>Latest</button> |
| 109 | <button onClick={() => pending && dispatch({ type: "send_confirmed", submissionId: pending.id })}>Accept</button> |
| 110 | <button onClick={event}>Identity</button><button onClick={() => record()}>Commit</button><button onClick={() => window.handoff.batched()}>Batched</button></div> |
| 111 | <Transcript items={current.items} tabId={tab} running={current.running} localSubmissions={orderedLocalSubmissions(current)} |
| 112 | visibleSubmissionHandoffs={current.visibleSubmissionHandoffs} localSubmissionSendRevision={current.localSubmissionSendRevision} |
| 113 | hasOlderHistory={current.historyHasOlder} hasNewerHistory={current.historyHasNewer} onPrompt={() => {}} onFork={() => {}} /> |
| 114 | <Composer running={current.running} collaborationMode="normal" toolApprovalMode="ask" modelLabel="Fixture" tabId={tab} |
| 115 | onSend={send} onCancel={async () => ({ discardedItemIds: [] })} onCycleMode={() => {}} onSetMode={() => {}} onSetCollaborationMode={() => {}} |
| 116 | onSetToolApprovalMode={() => {}} onClearGoal={() => {}} onPauseGoal={() => {}} onResumeGoal={() => {}} |
| 117 | onEditGoal={() => {}} |
| 118 | onSwitchModel={() => true} onSetEffort={() => {}} /> |
| 119 | </div>; |
| 120 | } |
| 121 | createRoot(document.getElementById("root")!).render(<LocaleProvider><ToastProvider><Fixture /></ToastProvider></LocaleProvider>); |
| 122 |