From d4105e04e5406b418b209651c71a80306c58956e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 29 Jul 2026 18:41:23 +0800 Subject: [PATCH] fix(app): own v2 rendering projections --- bun.lock | 7 +- .../performance/timeline-stability/fixture.ts | 12 +- packages/app/e2e/tsconfig.json | 1 + packages/app/src/context/server-sdk.tsx | 6 +- .../app/src/context/server-session.test.ts | 13 +- packages/app/src/types.ts | 215 ++++++++++++++++-- 6 files changed, 209 insertions(+), 45 deletions(-) diff --git a/bun.lock b/bun.lock index c08a107af9..e09ea82a5c 100644 --- a/bun.lock +++ b/bun.lock @@ -64,7 +64,6 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", - "@opencode-ai/sdk": "file:vendor/opencode-ai-sdk-1.18.8-dev.tgz", "@opencode-ai/session-ui": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/trees": "1.0.0-beta.4", @@ -4069,10 +4068,10 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "ffi-rs": ["ffi-rs@1.3.2", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.2", "@yuuang/ffi-rs-darwin-arm64": "1.3.2", "@yuuang/ffi-rs-darwin-x64": "1.3.2", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.2", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.2", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.2", "@yuuang/ffi-rs-linux-x64-musl": "1.3.2", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.2", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.2" } }, "sha512-4s8dX9VbBw/jd5NOuE3EJRqXaIVdjMyiumeeDzrOhtjQRwp6Bz2za7iksWXTnvTQKV/tTdm1s1w7mObe92zPjQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -6327,8 +6326,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@opencode-ai/sdk": ["@opencode-ai/sdk@vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="], - "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index dae65ff595..1e92439343 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -2,16 +2,8 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { Event } from "@opencode-ai/schema/event" import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event" import { SessionV1 } from "@opencode-ai/schema/session-v1" -import type { - AssistantMessage, - Message, - Part, - SessionV1Info, - SessionStatus, - ToolPart, - ToolState, - UserMessage, -} from "@opencode-ai/client/promise" +import type { SessionV1Info, SessionStatus } from "@opencode-ai/client/promise" +import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types" import { expect, type Page } from "@playwright/test" import { Schema } from "effect" import { mockOpenCodeServer } from "../../utils/mock-server" diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json index 4a6046e4fa..514c171993 100644 --- a/packages/app/e2e/tsconfig.json +++ b/packages/app/e2e/tsconfig.json @@ -12,6 +12,7 @@ "./performance/unit/visual-stability.test.ts", "./reproduction/timeline-suspense/**/*.ts", "./reproduction/timeline-suspense/**/*.tsx", + "../src/types.ts", "../src/pages/session/timeline/observe-element-offset.ts", "./regression/new-session-panel-corner.spec.ts", "./regression/session-timeline-context-resize.spec.ts", diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 2090b7b0af..18bfac476c 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -1,5 +1,5 @@ import type { OpenCodeEvent } from "@opencode-ai/client/promise" -import type { Event } from "@/types" +import type { Event, PermissionRequest } from "@/types" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { makeEventListener } from "@solid-primitives/event-listener" @@ -39,9 +39,9 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { event.data.source?.type === "tool" ? { messageID: event.data.source.messageID, callID: event.data.source.callID } : undefined, - }, + } satisfies PermissionRequest, current: event, - } as ServerEvent + } } return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent } diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 6fedd1a895..d0d3252fb0 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -28,6 +28,7 @@ const session = (id: string, parentID?: string): Session => ({ type UserMessage = Extract type AssistantMessage = Extract type TextPart = Extract +type CurrentToolObject = Extract["input"] type MessageResponse = { data: { info: Message; parts: Part[] }[] response: { headers: Headers } @@ -97,21 +98,21 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] { if (part.state.status === "running") return { status: "running" as const, - input: part.state.input, - metadata: part.state.metadata ?? {}, + input: part.state.input as CurrentToolObject, + metadata: (part.state.metadata ?? {}) as CurrentToolObject, } if (part.state.status === "error") return { status: "error" as const, - input: part.state.input, + input: part.state.input as CurrentToolObject, error: { type: "tool_error", message: part.state.error }, - metadata: part.state.metadata, + metadata: part.state.metadata as CurrentToolObject | undefined, } return { status: "completed" as const, - input: part.state.input, + input: part.state.input as CurrentToolObject, content: [{ type: "text" as const, text: part.state.output }], - metadata: part.state.metadata, + metadata: part.state.metadata as CurrentToolObject, } })() return [ diff --git a/packages/app/src/types.ts b/packages/app/src/types.ts index 2f3e8dac1a..41a7eb1f2d 100644 --- a/packages/app/src/types.ts +++ b/packages/app/src/types.ts @@ -1,59 +1,45 @@ import type { - AgentPart, - AssistantMessage, EventSubscribeOutput, FileDiffInfo, FileDiffLegacyInfo, - FilePart, - FilePartSource, - Message, - Part, Project, QuestionAnswer, QuestionInfo, - QuestionV2Request, + QuestionRequest, ReferenceInfo, SessionInfo, SessionNotFoundError, SessionStatus, SessionV1Info, SessionsResponse, - TextPart, - ToolPart, - UserMessage, } from "@opencode-ai/client/promise" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" export type { - AgentPart, - AssistantMessage, - FilePart, - FilePartSource, - Message, - Part, Project, QuestionAnswer, + QuestionRequest, ReferenceInfo, SessionNotFoundError, SessionStatus, - TextPart, - ToolPart, - UserMessage, } export type Session = SessionV1Info export type SessionV2Info = SessionInfo export type V2SessionListResponse = SessionsResponse -export type QuestionRequest = QuestionV2Request export type SnapshotFileDiff = FileDiffLegacyInfo export type VcsFileDiff = FileDiffInfo -export type Event = EventSubscribeOutput extends infer Item +type CurrentEvent = EventSubscribeOutput extends infer Item ? Item extends { type: infer Type extends string; data: infer Data } ? { type: Type; properties: Data } : never : never +export type Event = + | Exclude + | { type: "permission.asked"; properties: PermissionRequest } + export type EventSessionError = Extract export type PermissionRequest = { @@ -66,6 +52,193 @@ export type PermissionRequest = { tool?: { messageID: string; callID: string } } +type MessageError = + | { name: "ProviderAuthError"; data: { providerID: string; message: string } } + | { name: "UnknownError"; data: { message: string; ref?: string } } + | { name: "MessageOutputLengthError"; data: Record } + | { name: "MessageAbortedError"; data: { message: string } } + | { name: "StructuredOutputError"; data: { message: string; retries: number } } + | { name: "ContextOverflowError"; data: { message: string; responseBody?: string } } + | { name: "ContentFilterError"; data: { message: string } } + | { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: Record + responseBody?: string + metadata?: Record + } + } + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { created: number } + format?: { type: "text" } | { type: "json_schema"; schema: Record; retryCount?: number } + summary?: { title?: string; body?: string; diffs: SnapshotFileDiff[] } + agent: string + model: { providerID: string; modelID: string; variant?: string } + system?: string + tools?: Record +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { created: number; completed?: number } + error?: MessageError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { cwd: string; root: string } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { read: number; write: number } + } + structured?: unknown + variant?: string + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +type PartBase = { id: string; sessionID: string; messageID: string } + +export type TextPart = PartBase & { + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { start: number; end?: number } + metadata?: Record +} + +type FilePartSourceText = { value: string; start: number; end: number } +type FileSource = { text: FilePartSourceText; type: "file"; path: string } +type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: { start: { line: number; character: number }; end: { line: number; character: number } } + name: string + kind: number +} +type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = PartBase & { + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolState = + | { status: "pending"; input: Record; raw: string } + | { + status: "running" + input: Record + title?: string + metadata?: Record + time: { start: number } + } + | { + status: "completed" + input: Record + output: string + title: string + metadata: Record + time: { start: number; end: number; compacted?: number } + attachments?: FilePart[] + } + | { + status: "error" + input: Record + error: string + metadata?: Record + time: { start: number; end: number } + } + +export type ToolPart = PartBase & { + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: Record +} + +export type AgentPart = PartBase & { + type: "agent" + name: string + source?: { value: string; start: number; end: number } +} + +type ReasoningPart = PartBase & { + type: "reasoning" + text: string + metadata?: Record + time: { start: number; end?: number } +} + +type SubtaskPart = PartBase & { + type: "subtask" + prompt: string + description: string + agent: string + model?: { providerID: string; modelID: string } + command?: string +} + +type StepStartPart = PartBase & { type: "step-start"; snapshot?: string } +type StepFinishPart = PartBase & { + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: AssistantMessage["tokens"] +} +type SnapshotPart = PartBase & { type: "snapshot"; snapshot: string } +type PatchPart = PartBase & { type: "patch"; hash: string; files: string[] } +type RetryPart = PartBase & { + type: "retry" + attempt: number + error: Extract + time: { created: number } +} +type CompactionPart = PartBase & { + type: "compaction" + auto: boolean + overflow?: boolean + tail_start_id?: string +} + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + export type Todo = { content: string status: string