From 5c4ad74e417c8d5270616eac277b84c2b8d03b1a Mon Sep 17 00:00:00 2001 From: Yash Datta Date: Mon, 6 Jul 2026 00:31:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20web=20consumes=20@codeoid/protocol=20?= =?UTF-8?q?=E2=80=94=20retire=20the=20hand-maintained=20mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web UI kept a 700-line hand-synced copy of the wire types (web/src/protocol/types.ts, "keep them in sync"). It now depends on @codeoid/protocol (file:../packages/protocol — web stays a standalone package; bun's link: doesn't support relative paths) and the mirror file becomes a one-line re-export shim, so all existing "../protocol/types" imports resolve unchanged. One source of truth for daemon + web; the mobile client consumes the same package next. The switch immediately surfaced real drift the mirror had accumulated: web still typed and handled a "working" SessionStatus that was split into thinking/tool_running BEFORE v0.1.0 ever shipped — no published daemon has ever emitted it. Removed the dead handling (StatusDot, Transcript streaming gate, WorkerIndicator, approvals active-set, tests). Note on bun file: semantics — the dep is COPIED at install, not symlinked; after editing packages/protocol run `bun install` in web/ to refresh (documented in the shim header). CI installs fresh every run; --frozen-lockfile verified. Verified: web tsc clean, eslint clean, 171 tests pass, vite production build green (bundles the TS-source dep). Co-Authored-By: Claude Opus 4.8 (1M context) --- web/bun.lock | 3 + web/package.json | 1 + web/src/components/SessionListPane.tsx | 1 - web/src/components/transcript/Transcript.tsx | 1 - .../components/transcript/WorkerIndicator.tsx | 1 - web/src/lib/approvals.test.ts | 4 +- web/src/lib/approvals.ts | 1 - web/src/protocol/types.ts | 733 +----------------- web/src/state/sessions.test.ts | 4 +- 9 files changed, 18 insertions(+), 731 deletions(-) diff --git a/web/bun.lock b/web/bun.lock index 43cf96b..f18800e 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -5,6 +5,7 @@ "": { "name": "web", "dependencies": { + "@codeoid/protocol": "file:../packages/protocol", "@floating-ui/dom": "^1.7.6", "@tanstack/solid-virtual": "^3.13.24", "@types/diff": "^8.0.0", @@ -69,6 +70,8 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@codeoid/protocol": ["@codeoid/protocol@file:../packages/protocol", { "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], diff --git a/web/package.json b/web/package.json index 46b8e24..b8df878 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "test": "NODE_OPTIONS=\"--max-old-space-size=4096\" vitest run --pool=forks --poolOptions.forks.singleFork=true" }, "dependencies": { + "@codeoid/protocol": "file:../packages/protocol", "@floating-ui/dom": "^1.7.6", "@tanstack/solid-virtual": "^3.13.24", "@types/diff": "^8.0.0", diff --git a/web/src/components/SessionListPane.tsx b/web/src/components/SessionListPane.tsx index 1d6138e..10565b5 100644 --- a/web/src/components/SessionListPane.tsx +++ b/web/src/components/SessionListPane.tsx @@ -253,7 +253,6 @@ const SessionRow: Component<{ session: SessionInfo }> = (props) => { const StatusDot: Component<{ status: SessionStatus }> = (props) => { const cls = () => { switch (props.status) { - case "working": case "thinking": case "tool_running": return "bg-warn animate-pulse"; diff --git a/web/src/components/transcript/Transcript.tsx b/web/src/components/transcript/Transcript.tsx index 1782fb9..18a5c4a 100644 --- a/web/src/components/transcript/Transcript.tsx +++ b/web/src/components/transcript/Transcript.tsx @@ -70,7 +70,6 @@ const Transcript: Component = () => { const streamingMessageId = createMemo(() => { const status = focusedSession()?.status; if ( - status !== "working" && status !== "thinking" && status !== "tool_running" ) diff --git a/web/src/components/transcript/WorkerIndicator.tsx b/web/src/components/transcript/WorkerIndicator.tsx index a6e0bf8..324d3b8 100644 --- a/web/src/components/transcript/WorkerIndicator.tsx +++ b/web/src/components/transcript/WorkerIndicator.tsx @@ -26,7 +26,6 @@ const WorkerIndicator: Component = () => { const status = () => focusedSession()?.status; const visible = () => - status() === "working" || status() === "thinking" || status() === "tool_running"; diff --git a/web/src/lib/approvals.test.ts b/web/src/lib/approvals.test.ts index 4349214..2dbe144 100644 --- a/web/src/lib/approvals.test.ts +++ b/web/src/lib/approvals.test.ts @@ -45,7 +45,7 @@ describe("findPendingApproval", () => { it("finds a pending approval for every active status", () => { const arr = [msg("u1", "user"), waiting("m1", "ap-1")]; - const active: SessionStatus[] = ["waiting_approval", "working", "thinking", "tool_running"]; + const active: SessionStatus[] = ["waiting_approval", "thinking", "tool_running"]; for (const status of active) { expect(findPendingApproval(arr, status)?.messageId).toBe("m1"); } @@ -72,7 +72,7 @@ describe("findPendingApproval", () => { it("returns null when nothing is waiting", () => { const arr = [msg("u1", "user"), completedTool("m2"), msg("a1", "assistant")]; - expect(findPendingApproval(arr, "working")).toBeNull(); + expect(findPendingApproval(arr, "tool_running")).toBeNull(); expect(findPendingApproval([], "waiting_approval")).toBeNull(); }); diff --git a/web/src/lib/approvals.ts b/web/src/lib/approvals.ts index 91aba0c..ebfcf28 100644 --- a/web/src/lib/approvals.ts +++ b/web/src/lib/approvals.ts @@ -29,7 +29,6 @@ import type { SessionMessage, SessionStatus } from "../protocol/types"; const APPROVAL_POSSIBLE: ReadonlySet = new Set([ "waiting_approval", - "working", "thinking", "tool_running", ]); diff --git a/web/src/protocol/types.ts b/web/src/protocol/types.ts index 7fdee4c..9dbdadc 100644 --- a/web/src/protocol/types.ts +++ b/web/src/protocol/types.ts @@ -1,726 +1,13 @@ /** - * Wire-protocol types for the Codeoid daemon. Mirrors - * `codeoid/src/protocol/types.ts` — keep them in sync. + * Re-export shim. The wire protocol now comes from the shared + * `@codeoid/protocol` package (../../packages/protocol) — the same source of + * truth the daemon uses — instead of the hand-maintained mirror that lived + * here ("keep them in sync" is finally nobody's job). Kept so the existing + * `../protocol/types` imports across web/src resolve unchanged; new code may + * import from `@codeoid/protocol` directly. * - * Only the subset the web UI consumes is imported. Unknown fields on - * incoming messages are ignored gracefully (additive-safe protocol). + * NOTE (bun `file:` semantics): the dependency is COPIED into node_modules at + * install time, not symlinked. After editing `packages/protocol`, run + * `bun install` in `web/` to refresh the copy. CI installs fresh every run. */ - -export const PROTOCOL_VERSION = 1; - -/** - * Capability identifiers exchanged during the auth handshake. The client - * declares what it can consume on the `auth` frame; the daemon answers with - * what it can produce on `auth.ok`. Unknown strings are ignored, never - * rejected. - */ -export const CAPABILITIES = { - PARTS: "parts", - CHUNKED_REPLAY: "replay.chunked", - SEQ_RESUME: "replay.resume", - SEND_IDEMPOTENCY: "send.idempotency", -} as const; - -// ----------------------------------------------------------------------------- -// Identity + auth -// ----------------------------------------------------------------------------- - -export type IdentityType = "human" | "agent" | "subagent" | "system"; - -export interface MessageIdentity { - sub: string; - name?: string; - type: IdentityType; -} - -// ----------------------------------------------------------------------------- -// Sessions -// ----------------------------------------------------------------------------- - -// `working` + `waiting_approval` are what the daemon actually emits; the -// finer `thinking` / `tool_running` are kept for forward-compat in case the -// daemon ever splits the active state. Treat `working` as "active" in the UI. -export type SessionStatus = - | "idle" - | "working" - | "waiting_approval" - | "thinking" - | "tool_running" - | "error"; -export type SessionMode = "interactive" | "guarded" | "autonomous"; - -export interface SessionUsage { - inputTokens: number; - outputTokens: number; - cacheReadTokens: number; - cacheCreationTokens: number; - totalCostUsd: number; - numTurns: number; - durationMs: number; - recentTurns?: TurnUsage[]; - peakInputTokens?: number; - lastTurnInputTokens?: number; - lastTurnOutputTokens?: number; - lastTurnCostUsd?: number; - lastTurnCacheHitRate?: number; - /** - * Resolved model's context window in tokens — denominator for the - * ctx-occupancy display. Optional for back-compat with older daemons; - * frontends fall back to a conservative constant when unset. - */ - contextWindow?: number; -} - -export interface TurnUsage { - turnNumber: number; - createdAt: number; - inputTokens: number; - outputTokens: number; - cacheReadTokens: number; - cacheCreationTokens: number; - totalCostUsd: number; - durationMs: number; - stopReason?: string; - totalInputTokens: number; - billableInputTokens: number; - cacheHitRate: number; -} - -// ── Usage analytics ─────────────────────────────────────────────────────────── - -export interface DailyUsageBucket { - day: string; - costUsd: number; - inputTokens: number; - outputTokens: number; - numTurns: number; - numSessions: number; -} - -export interface LifetimeUsageTotals { - costUsd: number; - inputTokens: number; - outputTokens: number; - numTurns: number; - numSessions: number; -} - -export interface Subagent { - agentId: string; - wimseUri?: string; - agentType: string; - spawnedAt: number; - active: boolean; -} - -export interface SessionInfo { - id: string; - name: string; - workdir: string; - status: SessionStatus; - createdBy: string; - createdAt: string; - attachedClients: number; - mode?: SessionMode; - turnsRemaining?: number; - pinnedFiles?: string[]; - agentUri?: string; - subagents?: Subagent[]; - usage?: SessionUsage; - rotation?: { - count: number; - lastRotatedAt: number | null; - claudeCodeSessionId?: string; - }; - queuedMessages?: number; - model?: string; - fallbackModel?: string; -} - -// ----------------------------------------------------------------------------- -// Message content -// ----------------------------------------------------------------------------- - -export type MessageRole = - | "user" - | "assistant" - | "thinking" - | "tool_call" - | "tool_result" - | "system" - | "info"; - -export type ContentPart = - | { kind: "text"; text: string; markdown?: boolean } - | { kind: "code"; code: string; language?: string; filePath?: string } - | { - kind: "file_ref"; - path: string; - lines?: [number, number]; - change?: { added: number; removed: number }; - } - | { - kind: "diff"; - path: string; - added: number; - removed: number; - originalPath?: string; - } - | { kind: "tree"; label: string; children: TreeNode[] } - | { - kind: "button"; - label: string; - action: string; - data?: Record; - style?: "primary" | "secondary" | "danger"; - } - | { kind: "progress"; message: string; percent?: number; elapsedMs?: number } - | { kind: "image"; url: string; alt?: string } - | { kind: "anchor"; uri: string; title: string } - | { kind: "table"; headers: string[]; rows: string[][] }; - -export interface TreeNode { - label: string; - type: "file" | "directory"; - path?: string; - children?: TreeNode[]; -} - -// ----------------------------------------------------------------------------- -// Tool lifecycle -// ----------------------------------------------------------------------------- - -export type ToolPhase = - | "streaming" - | "waiting_confirmation" - | "executing" - | "completed" - | "cancelled"; - -export type ToolState = - | { phase: "streaming"; partialInput?: unknown } - | { - phase: "waiting_confirmation"; - input: unknown; - description: string; - approvalId: string; - } - | { phase: "executing"; progress?: string; elapsedMs?: number } - | { - phase: "completed"; - success: boolean; - output?: string; - elapsedMs?: number; - confirmedBy?: "user" | "auto" | "setting"; - } - | { - phase: "cancelled"; - reason: "denied" | "interrupted" | "timeout"; - message?: string; - }; - -export interface ToolInfo { - toolId: string; - name: string; - state: ToolState; - /** - * Original tool input from the model. Persists across phase - * transitions so the diff renderer can show the proposed Edit / - * Write even after the tool moves out of waiting_confirmation. - */ - input?: unknown; -} - -// ----------------------------------------------------------------------------- -// Session messages -// ----------------------------------------------------------------------------- - -export interface SessionMessage { - type: "session.message"; - sessionId: string; - messageId: string; - role: MessageRole; - content: string; - parts?: ContentPart[]; - identity: MessageIdentity; - tool?: ToolInfo; - metadata?: Record; - timestamp: string; - /** Session sequence cursor (`replay.resume`) — track max(seq) per session. */ - seq?: number; -} - -export interface SessionMessageDelta { - type: "session.message.delta"; - sessionId: string; - messageId: string; - contentAppend?: string; - partsAppend?: ContentPart[]; - partsUpdate?: { index: number; part: ContentPart }[]; - toolStateUpdate?: ToolState; - timestamp: string; - /** Session sequence cursor (`replay.resume`) — track max(seq) per session. */ - seq?: number; -} - -// ----------------------------------------------------------------------------- -// Client → Daemon messages -// ----------------------------------------------------------------------------- - -interface BaseClientMsg { - id: string; -} - -export interface SessionCreateMsg extends BaseClientMsg { - type: "session.create"; - name: string; - workdir: string; -} - -export interface SessionRenameMsg extends BaseClientMsg { - type: "session.rename"; - sessionId: string; - name: string; -} - -export interface SessionListMsg extends BaseClientMsg { - type: "session.list"; -} - -export interface SessionAttachMsg extends BaseClientMsg { - type: "session.attach"; - sessionId: string; - /** - * Incremental resume (`replay.resume`): pass the `resumeKey` + highest - * `seq` from previous frames to receive only the tail mutated since, - * instead of a full scrollback replay. Daemon falls back to a snapshot - * on any key mismatch. - */ - resume?: { key: string; sinceSeq: number }; -} - -export interface SessionDetachMsg extends BaseClientMsg { - type: "session.detach"; - sessionId: string; -} - -export interface SessionSendMsg extends BaseClientMsg { - type: "session.send"; - sessionId: string; - text: string; - attachments?: { path: string; content?: string; mimeType?: string; data?: string }[]; - priority?: "now" | "next" | "later"; - /** - * Idempotency key (`send.idempotency`): generated once per user action; - * the daemon acks duplicates instead of running a second turn, so an - * ambiguous socket drop + retry can't double-bill a prompt. - */ - clientMsgId?: string; -} - -export interface SessionInterruptMsg extends BaseClientMsg { - type: "session.interrupt"; - sessionId: string; -} - -export interface SessionApproveMsg extends BaseClientMsg { - type: "session.approve"; - sessionId: string; - approvalId: string; - approved: boolean; - /** - * Patch shallow-merged into the original tool input before the SDK - * runs the tool. Required for AskUserQuestion (carries `answers`). - * Omitted for binary approvals. - */ - updatedInput?: Record; -} - -export interface SessionDestroyMsg extends BaseClientMsg { - type: "session.destroy"; - sessionId: string; -} - -export interface SessionSetModeMsg extends BaseClientMsg { - type: "session.set_mode"; - sessionId: string; - mode: SessionMode; - maxTurns?: number; -} - -export interface SessionRotateMsg extends BaseClientMsg { - type: "session.rotate"; - sessionId: string; -} - -export interface SessionSetModelMsg extends BaseClientMsg { - type: "session.set_model"; - sessionId: string; - model: string; - fallbackModel?: string | null; -} - -export interface SessionSearchMsg extends BaseClientMsg { - type: "session.search"; - query: string; - scope?: "workspace" | "all"; - workdir?: string; - limit?: number; -} - -export interface FsListMsg extends BaseClientMsg { - type: "fs.list"; - sessionId: string; - /** Path relative to the session's workdir. "" / "." / "/" all = root. */ - path: string; -} - -export interface FsReadMsg extends BaseClientMsg { - type: "fs.read"; - sessionId: string; - path: string; - /** Hard cap in bytes; daemon ceiling is 10 MiB. Default 1 MiB. */ - maxBytes?: number; -} - -export interface FsBrowseDirMsg extends BaseClientMsg { - type: "fs.browse_dir"; - /** Absolute path to browse. Defaults to the daemon user's HOME. */ - path?: string; -} - -export interface ClaudeConfigMsg extends BaseClientMsg { - type: "claude.config"; - sessionId: string; -} - -/** Ask the daemon for the live model catalog the backend supports. */ -export interface ModelsListMsg extends BaseClientMsg { - type: "models.list"; -} - -/** One selectable model as reported by the Claude Code backend. */ -export interface ModelInfo { - value: string; - displayName: string; - description?: string; - isDefault?: boolean; -} - -export interface ModelsListResultMsg { - type: "models.list.result"; - requestId: string; - models: ModelInfo[]; - live: boolean; - /** Provider whose catalog this is (e.g. "claude", "gemini"). */ - provider: string; -} - -export interface SessionExportMsg extends BaseClientMsg { - type: "session.export"; - sessionId: string; - includeMemory?: boolean; - includePinnedFiles?: boolean; - aliasOverride?: string; - toFile?: boolean; -} - -export interface SessionImportMsg extends BaseClientMsg { - type: "session.import"; - source: { kind: "inline"; bundle: unknown } | { kind: "file"; path: string }; - targetWorkdir: string; - nameOverride?: string; - writePinnedFiles?: boolean; -} - -/** Liveness heartbeat — daemon replies with `response.ok`. */ -export interface PingMsg extends BaseClientMsg { - type: "ping"; -} - -export interface UsageDailyMsg extends BaseClientMsg { - type: "usage.daily"; - days?: number; -} - -export type ClientMessage = - | PingMsg - | SessionCreateMsg - | SessionRenameMsg - | SessionListMsg - | SessionAttachMsg - | SessionDetachMsg - | SessionSendMsg - | SessionInterruptMsg - | SessionApproveMsg - | SessionDestroyMsg - | SessionSetModeMsg - | SessionRotateMsg - | SessionSetModelMsg - | SessionSearchMsg - | FsListMsg - | FsReadMsg - | FsBrowseDirMsg - | ClaudeConfigMsg - | ModelsListMsg - | SessionExportMsg - | SessionImportMsg - | UsageDailyMsg; - -// ----------------------------------------------------------------------------- -// Daemon → Client messages -// ----------------------------------------------------------------------------- - -export interface AuthOkMsg { - type: "auth.ok"; - identity: MessageIdentity; - scopes: readonly string[]; - protocolVersion?: number; - /** Capabilities the daemon supports — feature-detect on this. */ - capabilities?: string[]; -} - -export interface ResponseOkMsg { - type: "response.ok"; - requestId: string; - data?: unknown; -} - -export type ErrorCode = - | "unauthorized" - | "forbidden" - | "not_found" - | "invalid_request" - | "rate_limited" - | "internal"; - -export interface ResponseErrorMsg { - type: "response.error"; - requestId: string; - error: string; - code: ErrorCode; -} - -export interface SessionListResultMsg { - type: "session.list.result"; - requestId: string; - sessions: SessionInfo[]; -} - -export interface SessionStatusChangeMsg { - type: "session.status_change"; - sessionId: string; - status: SessionStatus; - timestamp: string; -} - -export interface SessionInfoUpdateMsg { - type: "session.info_update"; - session: SessionInfo; - timestamp: string; -} - -export interface ScrollbackReplayMsg { - type: "scrollback.replay"; - sessionId: string; - messages: SessionMessage[]; - /** - * Chunked replay (#84, additive & optional). When set, the daemon split a - * large scrollback into ordered chunks (oldest→newest). Reset scrollback - * when `seq` is absent or 0; append when `seq > 0`; the replay is complete - * on `final` (or when `seq` is absent — a single-frame legacy replay). - * NOTE: this is the CHUNK index — unrelated to `SessionMessage.seq`. - */ - seq?: number; - final?: boolean; - /** - * Replay semantics (`replay.resume`): "snapshot" (or absent) = reset the - * local buffer to this replay; "incremental" = append/upsert only — the - * daemon sent just the tail mutated since our resume cursor. - */ - mode?: "snapshot" | "incremental"; - /** Replay-buffer identity — store with maxSeq, pass back on attach.resume. */ - resumeKey?: string; - /** Highest session seq included/known — the next resume cursor. */ - maxSeq?: number; -} - -export interface SessionSearchSnippet { - episodeId: string; - kind: "user_turn" | "assistant_turn" | "tool_call" | "error"; - toolName?: string; - summary: string; - excerpt: string; - createdAt: number; - score: number; - filePaths: string[]; -} - -export interface SessionSearchHit { - sessionId: string; - sessionName: string; - workdir: string; - matchCount: number; - firstMatchAt: number; - lastMatchAt: number; - aggregateScore: number; - snippets: SessionSearchSnippet[]; -} - -export interface SessionSearchResultMsg { - type: "session.search.result"; - requestId: string; - query: string; - sessions: SessionSearchHit[]; - workspaceId: string; - limit: number; -} - -export interface FsEntry { - name: string; - /** Path relative to the session's workdir. */ - path: string; - kind: "file" | "directory"; - size?: number; - mtimeMs?: number; - isSymlink?: boolean; -} - -export interface FsListResultMsg { - type: "fs.list.result"; - requestId: string; - /** Echoed `path` (canonicalised, still relative to workdir). */ - path: string; - entries: FsEntry[]; -} - -export interface FsReadResultMsg { - type: "fs.read.result"; - requestId: string; - path: string; - /** UTF-8 text or base64-encoded binary; decide via `encoding`. */ - content: string; - encoding: "utf-8" | "base64"; - /** Total size on disk; may exceed `content` length when `truncated`. */ - size: number; - /** shiki-compatible language hint, when the daemon could detect one. */ - language?: string; - truncated: boolean; -} - -export interface FsBrowseDirResultMsg { - type: "fs.browse_dir.result"; - requestId: string; - path: string; - root: string; - parent: string | null; - entries: FsEntry[]; -} - -export type ClaudeConfigScope = "global" | "workdir"; - -export interface ClaudeConfigAgent { - name: string; - description: string | null; - path: string; - scope: ClaudeConfigScope; - tools?: string[]; -} - -export interface ClaudeConfigSkill { - name: string; - description: string | null; - path: string; - scope: ClaudeConfigScope; -} - -export interface ClaudeConfigMcpServer { - name: string; - scope: ClaudeConfigScope; - path: string; - command: string | null; - args: string[]; - envKeys: string[]; - url: string | null; - type: string | null; - /** HTTP-type MCP servers' header keys (values redacted at the daemon). */ - headerKeys?: string[]; - /** SDK-reported live connection status (e.g. "connected", "failed"). */ - liveStatus?: string; - /** Fully-qualified MCP tool names exposed by this server in the active session. */ - liveTools?: string[]; -} - -export interface ClaudeConfigHook { - event: string; - scope: ClaudeConfigScope; - path: string; - matcher: string | null; - kind: string; - command: string; -} - -export interface ClaudeConfigResultMsg { - type: "claude.config.result"; - requestId: string; - workdir: string; - agents: ClaudeConfigAgent[]; - skills: ClaudeConfigSkill[]; - mcpServers: ClaudeConfigMcpServer[]; - hooks: ClaudeConfigHook[]; -} - -export interface SessionExportResultMsg { - type: "session.export.result"; - requestId: string; - manifest: { - exportedAt: string; - session: { - id: string; - name: string; - createdAt: string; - model?: string; - mode?: string; - }; - workdir: { alias: string; aliasSource: string; originalAbsolute: string }; - counts: { - messages: number; - episodes: number; - turns: number; - pinnedFiles: number; - }; - }; - payload: - | { kind: "inline"; bundle: unknown; sizeBytes: number } - | { kind: "file"; path: string; sizeBytes: number }; -} - -export interface SessionImportResultMsg { - type: "session.import.result"; - requestId: string; - newSessionId: string; - importedMessages: number; - importedEpisodes: number; - importedTurns: number; - pinnedFilesWritten: number; - warnings: string[]; -} - -export type DaemonMessage = - | AuthOkMsg - | ResponseOkMsg - | ResponseErrorMsg - | SessionListResultMsg - | SessionMessage - | SessionMessageDelta - | SessionStatusChangeMsg - | SessionInfoUpdateMsg - | ScrollbackReplayMsg - | SessionSearchResultMsg - | FsListResultMsg - | FsReadResultMsg - | FsBrowseDirResultMsg - | ClaudeConfigResultMsg - | ModelsListResultMsg - | SessionExportResultMsg - | SessionImportResultMsg; +export * from "@codeoid/protocol"; diff --git a/web/src/state/sessions.test.ts b/web/src/state/sessions.test.ts index 824127b..67d6542 100644 --- a/web/src/state/sessions.test.ts +++ b/web/src/state/sessions.test.ts @@ -105,11 +105,11 @@ describe("sessions store", () => { // Fresh payload objects, same field values for "a", changed status for "b". ingestSessionList([ s("a", "2026-05-01T08:00:00Z", { name: "alpha" }), - s("b", "2026-05-02T08:00:00Z", { status: "working" }), + s("b", "2026-05-02T08:00:00Z", { status: "thinking" }), ]); expect(getSession("a")).toBe(before!); expect(getSession("a")?.name).toBe("alpha"); - expect(getSession("b")?.status).toBe("working"); + expect(getSession("b")?.status).toBe("thinking"); }); it("ingestSessionList updates changed fields, adds new sessions, deletes missing ones", () => {