From 2a7e32c41654ce38d722c5a8a663e168e1162f85 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 11 Jul 2026 16:29:06 -0400 Subject: [PATCH] refactor(tui): remove legacy sdk surfaces --- .../client/src/promise/generated/types.ts | 269 +++++----- packages/core/src/integration.ts | 2 +- packages/core/src/reference.ts | 4 +- packages/core/test/integration.test.ts | 2 +- packages/core/test/plugin/models-dev.test.ts | 2 +- packages/core/test/reference-guidance.test.ts | 6 +- packages/core/test/reference.test.ts | 6 +- packages/httpapi-codegen/src/index.ts | 5 +- packages/schema/src/integration.ts | 5 +- packages/schema/src/reference.ts | 5 +- packages/schema/src/shell.ts | 10 +- packages/tui/src/app.tsx | 52 -- .../tui/src/component/dialog-console-org.tsx | 135 ----- .../tui/src/component/dialog-integration.tsx | 8 +- packages/tui/src/component/dialog-mcp.tsx | 2 +- .../tui/src/component/dialog-move-session.tsx | 16 +- .../tui/src/component/dialog-provider.tsx | 469 ------------------ .../tui/src/component/dialog-session-list.tsx | 2 +- packages/tui/src/component/dialog-skill.tsx | 2 +- .../src/component/dialog-workspace-create.tsx | 308 ------------ .../dialog-workspace-file-changes.tsx | 2 +- .../src/component/dialog-workspace-list.tsx | 112 ----- .../dialog-workspace-unavailable.tsx | 69 --- .../tui/src/component/prompt/autocomplete.tsx | 2 +- packages/tui/src/component/prompt/index.tsx | 86 +--- .../tui/src/component/prompt/workspace.tsx | 137 ----- .../tui/src/component/workspace-label.tsx | 19 - packages/tui/src/config/keybind.ts | 4 - packages/tui/src/context/data.tsx | 8 +- packages/tui/src/context/local.tsx | 2 +- packages/tui/src/context/location.tsx | 2 +- packages/tui/src/context/project.tsx | 52 +- packages/tui/src/context/sync.tsx | 20 - .../feature-plugins/system/diff-viewer.tsx | 23 +- .../src/routes/session/dialog-timeline.tsx | 15 +- packages/tui/src/routes/session/footer.tsx | 2 +- packages/tui/src/routes/session/form.tsx | 2 +- packages/tui/src/routes/session/index.tsx | 2 +- .../tui/src/routes/session/permission.tsx | 2 +- packages/tui/src/routes/session/rows.ts | 2 +- packages/tui/src/routes/session/sidebar.tsx | 24 +- packages/tui/src/util/connected-provider.ts | 2 +- packages/tui/src/util/model.ts | 24 - packages/tui/src/util/provider-origin.ts | 7 - packages/tui/src/util/session.ts | 2 +- packages/tui/src/util/transcript.ts | 112 ----- .../cmd/tui/dialog-workspace-create.test.ts | 28 -- .../cli/cmd/tui/integration-options.test.ts | 2 +- .../test/cli/cmd/tui/notifications.test.ts | 8 +- .../test/cli/cmd/tui/provider-options.test.ts | 41 -- .../tui/test/cli/tui/diff-viewer.test.tsx | 54 +- .../tui/test/cli/tui/session-rows.test.ts | 2 +- packages/tui/test/util/session.test.ts | 2 +- packages/tui/test/util/transcript.test.ts | 421 ---------------- 54 files changed, 278 insertions(+), 2324 deletions(-) delete mode 100644 packages/tui/src/component/dialog-console-org.tsx delete mode 100644 packages/tui/src/component/dialog-provider.tsx delete mode 100644 packages/tui/src/component/dialog-workspace-create.tsx delete mode 100644 packages/tui/src/component/dialog-workspace-list.tsx delete mode 100644 packages/tui/src/component/dialog-workspace-unavailable.tsx delete mode 100644 packages/tui/src/component/prompt/workspace.tsx delete mode 100644 packages/tui/src/component/workspace-label.tsx delete mode 100644 packages/tui/src/util/provider-origin.ts delete mode 100644 packages/tui/src/util/transcript.ts delete mode 100644 packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts delete mode 100644 packages/tui/test/cli/cmd/tui/provider-options.test.ts delete mode 100644 packages/tui/test/util/transcript.test.ts diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 3ee55ed4e8..d5458300b6 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -130,9 +130,9 @@ export type SessionMessageCompactionCompleted = { export type InstructionEntryKey = string -export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: unknown } } +export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } -export type Shell = { +export type ShellInfo = { id: string status: "running" | "exited" | "timeout" | "killed" command: string @@ -141,19 +141,19 @@ export type Shell = { file: string pid?: number exit?: number - metadata: { [x: string]: unknown } + metadata: { [x: string]: JsonValue } time: { started: number; completed?: number } } -export type SessionMessageProviderState3 = { [x: string]: unknown } +export type SessionMessageProviderState3 = { [x: string]: JsonValue } -export type SessionMessageProviderState4 = { [x: string]: unknown } +export type SessionMessageProviderState4 = { [x: string]: JsonValue } -export type SessionMessageProviderState5 = { [x: string]: unknown } +export type SessionMessageProviderState5 = { [x: string]: JsonValue } -export type SessionMessageProviderState6 = { [x: string]: unknown } +export type SessionMessageProviderState6 = { [x: string]: JsonValue } -export type SessionMessageProviderState7 = { [x: string]: unknown } +export type SessionMessageProviderState7 = { [x: string]: JsonValue } export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: number } @@ -445,7 +445,7 @@ export type QuestionV2Tool = { messageID: string; callID: string } export type QuestionV2Answer = Array -export type FormMetadata1 = { [x: string]: unknown } +export type FormMetadata1 = { [x: string]: JsonValue } export type FormWhen1 = { key: string; op: "eq" | "neq"; value: string | number | boolean } @@ -466,7 +466,7 @@ export type QuestionTool = { messageID: string; callID: string } export type QuestionAnswer = Array -export type Shell1 = { +export type ShellInfo1 = { id: string status: "running" | "exited" | "timeout" | "killed" command: string @@ -474,9 +474,9 @@ export type Shell1 = { shell: string file: string pid?: number - exit?: number | "Infinity" | "-Infinity" | "NaN" + exit?: number metadata: { [x: string]: JsonValue } - time: { started: number | "Infinity" | "-Infinity" | "NaN"; completed?: number | "Infinity" | "-Infinity" | "NaN" } + time: { started: number; completed?: number } } export type ReferenceLocalSource = { type: "local"; path: string; description?: string; hidden?: boolean } @@ -527,7 +527,7 @@ export type PermissionV2Rule = { action: string; resource: string; effect: Permi export type SessionAgentSelected = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.agent.selected" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -537,7 +537,7 @@ export type SessionAgentSelected = { export type SessionModelSelected = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.model.selected" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -547,7 +547,7 @@ export type SessionModelSelected = { export type SessionMoved = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.moved" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -557,7 +557,7 @@ export type SessionMoved = { export type SessionRenamed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.renamed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -567,7 +567,7 @@ export type SessionRenamed = { export type SessionDeleted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.deleted" durable: { aggregateID: string; seq: number; version: 2 } location?: LocationRef @@ -577,7 +577,7 @@ export type SessionDeleted = { export type SessionForked = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.forked" durable: { aggregateID: string; seq: number; version: 2 } location?: LocationRef @@ -587,7 +587,7 @@ export type SessionForked = { export type SessionInputPromoted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.input.promoted" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -597,7 +597,7 @@ export type SessionInputPromoted = { export type SessionExecutionStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.execution.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -607,7 +607,7 @@ export type SessionExecutionStarted = { export type SessionExecutionSucceeded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.execution.succeeded" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -617,7 +617,7 @@ export type SessionExecutionSucceeded = { export type SessionExecutionInterrupted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.execution.interrupted" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -627,7 +627,7 @@ export type SessionExecutionInterrupted = { export type SessionInstructionsUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.instructions.updated" durable: { aggregateID: string; seq: number; version: 2 } location?: LocationRef @@ -637,17 +637,17 @@ export type SessionInstructionsUpdated = { export type SessionSynthetic = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.synthetic" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; text: string; description?: string; metadata?: { [x: string]: unknown } } + data: { sessionID: string; text: string; description?: string; metadata?: { [x: string]: JsonValue } } } export type SessionSkillActivated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.skill.activated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -657,7 +657,7 @@ export type SessionSkillActivated = { export type SessionStepStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.step.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -667,7 +667,7 @@ export type SessionStepStarted = { export type SessionStepEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.step.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -685,7 +685,7 @@ export type SessionStepEnded = { export type SessionTextStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.text.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -695,7 +695,7 @@ export type SessionTextStarted = { export type SessionTextEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.text.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -705,7 +705,7 @@ export type SessionTextEnded = { export type SessionToolInputStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.input.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -715,7 +715,7 @@ export type SessionToolInputStarted = { export type SessionToolInputEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.input.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -725,7 +725,7 @@ export type SessionToolInputEnded = { export type SessionCompactionAdmitted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.compaction.admitted" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -735,7 +735,7 @@ export type SessionCompactionAdmitted = { export type SessionCompactionStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.compaction.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -745,7 +745,7 @@ export type SessionCompactionStarted = { export type SessionCompactionEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.compaction.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -755,7 +755,7 @@ export type SessionCompactionEnded = { export type SessionRevertCleared = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.revert.cleared" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -765,7 +765,7 @@ export type SessionRevertCleared = { export type SessionRevertCommitted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.revert.committed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -775,7 +775,7 @@ export type SessionRevertCommitted = { export type ModelsDevRefreshed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "models-dev.refreshed" location?: LocationRef data: {} @@ -784,7 +784,7 @@ export type ModelsDevRefreshed = { export type IntegrationUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "integration.updated" location?: LocationRef data: {} @@ -793,7 +793,7 @@ export type IntegrationUpdated = { export type IntegrationConnectionUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "integration.connection.updated" location?: LocationRef data: { integrationID: string } @@ -802,7 +802,7 @@ export type IntegrationConnectionUpdated = { export type CatalogUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "catalog.updated" location?: LocationRef data: {} @@ -811,7 +811,7 @@ export type CatalogUpdated = { export type AgentUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "agent.updated" location?: LocationRef data: {} @@ -820,7 +820,7 @@ export type AgentUpdated = { export type MessageRemoved = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "message.removed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -830,7 +830,7 @@ export type MessageRemoved = { export type MessagePartRemoved = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "message.part.removed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -840,7 +840,7 @@ export type MessagePartRemoved = { export type SessionUsageUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.usage.updated" location?: LocationRef data: { sessionID: string; cost: MoneyUSD; tokens: TokenUsageInfo } @@ -849,7 +849,7 @@ export type SessionUsageUpdated = { export type SessionTextDelta = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.text.delta" location?: LocationRef data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } @@ -858,7 +858,7 @@ export type SessionTextDelta = { export type SessionReasoningDelta = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.reasoning.delta" location?: LocationRef data: { sessionID: string; assistantMessageID: string; ordinal: number; delta: string } @@ -867,7 +867,7 @@ export type SessionReasoningDelta = { export type SessionToolInputDelta = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.input.delta" location?: LocationRef data: { sessionID: string; assistantMessageID: string; callID: string; delta: string } @@ -876,7 +876,7 @@ export type SessionToolInputDelta = { export type SessionCompactionDelta = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.compaction.delta" location?: LocationRef data: { sessionID: string; text: string } @@ -885,7 +885,7 @@ export type SessionCompactionDelta = { export type FilesystemChanged = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "filesystem.changed" location?: LocationRef data: { file: string; event: "add" | "change" | "unlink" } @@ -894,7 +894,7 @@ export type FilesystemChanged = { export type ReferenceUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "reference.updated" location?: LocationRef data: {} @@ -903,7 +903,7 @@ export type ReferenceUpdated = { export type PluginAdded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "plugin.added" location?: LocationRef data: { id: string } @@ -912,7 +912,7 @@ export type PluginAdded = { export type PluginUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "plugin.updated" location?: LocationRef data: {} @@ -921,7 +921,7 @@ export type PluginUpdated = { export type ProjectDirectoriesUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "project.directories.updated" location?: LocationRef data: { projectID: string } @@ -930,7 +930,7 @@ export type ProjectDirectoriesUpdated = { export type CommandUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "command.updated" location?: LocationRef data: {} @@ -939,7 +939,7 @@ export type CommandUpdated = { export type ConfigUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "config.updated" location?: LocationRef data: {} @@ -948,7 +948,7 @@ export type ConfigUpdated = { export type SkillUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "skill.updated" location?: LocationRef data: {} @@ -957,7 +957,7 @@ export type SkillUpdated = { export type PtyExited = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "pty.exited" location?: LocationRef data: { id: string; exitCode: number } @@ -966,7 +966,7 @@ export type PtyExited = { export type PtyDeleted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "pty.deleted" location?: LocationRef data: { id: string } @@ -975,7 +975,7 @@ export type PtyDeleted = { export type ShellExited = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "shell.exited" location?: LocationRef data: { id: string; exit?: number; status: "running" | "exited" | "timeout" | "killed" } @@ -984,7 +984,7 @@ export type ShellExited = { export type ShellDeleted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "shell.deleted" location?: LocationRef data: { id: string } @@ -993,7 +993,7 @@ export type ShellDeleted = { export type QuestionV2Rejected = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.v2.rejected" location?: LocationRef data: { sessionID: string; requestID: string } @@ -1002,7 +1002,7 @@ export type QuestionV2Rejected = { export type FormCancelled = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "form.cancelled" location?: LocationRef data: { id: string; sessionID: string } @@ -1011,7 +1011,7 @@ export type FormCancelled = { export type SessionIdle = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.idle" location?: LocationRef data: { sessionID: string } @@ -1020,7 +1020,7 @@ export type SessionIdle = { export type TuiPromptAppend = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "tui.prompt.append" location?: LocationRef data: { text: string } @@ -1029,7 +1029,7 @@ export type TuiPromptAppend = { export type TuiCommandExecute = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "tui.command.execute" location?: LocationRef data: { @@ -1058,7 +1058,7 @@ export type TuiCommandExecute = { export type TuiToastShow = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "tui.toast.show" location?: LocationRef data: { @@ -1072,7 +1072,7 @@ export type TuiToastShow = { export type TuiSessionSelect = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "tui.session.select" location?: LocationRef data: { sessionID: string } @@ -1081,7 +1081,7 @@ export type TuiSessionSelect = { export type InstallationUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "installation.updated" location?: LocationRef data: { version: string } @@ -1090,7 +1090,7 @@ export type InstallationUpdated = { export type InstallationUpdateAvailable = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "installation.update-available" location?: LocationRef data: { version: string } @@ -1099,7 +1099,7 @@ export type InstallationUpdateAvailable = { export type VcsBranchUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "vcs.branch.updated" location?: LocationRef data: { branch?: string } @@ -1108,7 +1108,7 @@ export type VcsBranchUpdated = { export type McpStatusChanged = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "mcp.status.changed" location?: LocationRef data: { server: string } @@ -1117,7 +1117,7 @@ export type McpStatusChanged = { export type McpResourcesChanged = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "mcp.resources.changed" location?: LocationRef data: { server: string } @@ -1126,7 +1126,7 @@ export type McpResourcesChanged = { export type PermissionAsked = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "permission.asked" location?: LocationRef data: { @@ -1134,7 +1134,7 @@ export type PermissionAsked = { sessionID: string permission: string patterns: Array - metadata: { [x: string]: unknown } + metadata: { [x: string]: JsonValue } always: Array tool?: { messageID: string; callID: string } | undefined } @@ -1143,7 +1143,7 @@ export type PermissionAsked = { export type PermissionReplied = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "permission.replied" location?: LocationRef data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } @@ -1152,7 +1152,7 @@ export type PermissionReplied = { export type QuestionRejected = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.rejected" location?: LocationRef data: { sessionID: string; requestID: string } @@ -1160,7 +1160,7 @@ export type QuestionRejected = { export type V2EventServerConnected = { id: string - metadata?: { [x: string]: unknown } | undefined + metadata?: { [x: string]: JsonValue } | undefined location?: LocationRef | undefined type: "server.connected" data: {} @@ -1213,7 +1213,7 @@ export type SessionMessageCompactionFailed = { export type SessionExecutionFailed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.execution.failed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1223,7 +1223,7 @@ export type SessionExecutionFailed = { export type SessionStepFailed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.step.failed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1239,7 +1239,7 @@ export type SessionStepFailed = { export type SessionRetryScheduled = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.retry.scheduled" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1249,7 +1249,7 @@ export type SessionRetryScheduled = { export type SessionCompactionFailed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.compaction.failed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1267,23 +1267,23 @@ export type SessionPendingSyntheticMessage = { export type SessionShellStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.shell.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; shell: Shell } + data: { sessionID: string; shell: ShellInfo } } export type SessionShellEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.shell.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef data: { sessionID: string - shell: Shell + shell: ShellInfo output: { output: string; cursor: number; size: number; truncated: boolean } } } @@ -1291,16 +1291,16 @@ export type SessionShellEnded = { export type ShellCreated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "shell.created" location?: LocationRef - data: { info: Shell } + data: { info: ShellInfo } } export type SessionReasoningStarted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.reasoning.started" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1310,7 +1310,7 @@ export type SessionReasoningStarted = { export type SessionReasoningEnded = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.reasoning.ended" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1326,7 +1326,7 @@ export type SessionReasoningEnded = { export type SessionToolCalled = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.called" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1334,7 +1334,7 @@ export type SessionToolCalled = { sessionID: string assistantMessageID: string callID: string - input: { [x: string]: unknown } + input: { [x: string]: JsonValue } executed: boolean state?: SessionMessageProviderState5 } @@ -1343,7 +1343,7 @@ export type SessionToolCalled = { export type SessionToolFailed = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.failed" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1352,7 +1352,7 @@ export type SessionToolFailed = { assistantMessageID: string callID: string error: SessionStructuredError - result?: unknown + result?: JsonValue executed: boolean resultState?: SessionMessageProviderState7 } @@ -1490,7 +1490,7 @@ export type PermissionV2Request = { export type PermissionV2Asked = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "permission.v2.asked" location?: LocationRef data: { @@ -1499,7 +1499,7 @@ export type PermissionV2Asked = { action: string resources: Array save?: Array - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } source?: PermissionV2Source } } @@ -1558,7 +1558,7 @@ export type RetryPart = { export type SessionError = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.error" location?: LocationRef data: { @@ -1592,7 +1592,7 @@ export type SymbolSource = { export type PermissionV2Replied = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "permission.v2.replied" location?: LocationRef data: { sessionID: string; requestID: string; reply: PermissionV2Reply } @@ -1601,7 +1601,7 @@ export type PermissionV2Replied = { export type PtyCreated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "pty.created" location?: LocationRef data: { info: Pty } @@ -1610,7 +1610,7 @@ export type PtyCreated = { export type PtyUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "pty.updated" location?: LocationRef data: { info: Pty } @@ -1627,7 +1627,7 @@ export type QuestionV2Info = { export type QuestionV2Replied = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.v2.replied" location?: LocationRef data: { sessionID: string; requestID: string; answers: Array } @@ -1701,7 +1701,7 @@ export type FormMultiselectField1 = { export type SessionStatus2 = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.status" location?: LocationRef data: { sessionID: string; status: SessionStatus } @@ -1718,7 +1718,7 @@ export type QuestionInfo = { export type QuestionReplied = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.replied" location?: LocationRef data: { sessionID: string; requestID: string; answers: Array } @@ -1747,7 +1747,7 @@ export type SessionInfo = { export type SessionRevertStaged = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.revert.staged" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1775,7 +1775,7 @@ export type SessionPendingUserData1 = { text: string files?: Array agents?: Array - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } } export type SessionMessageToolStateRunning = { @@ -1805,7 +1805,7 @@ export type SessionMessageToolStateError = { export type SessionToolProgress = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.progress" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1813,7 +1813,7 @@ export type SessionToolProgress = { sessionID: string assistantMessageID: string callID: string - structured: { [x: string]: unknown } + structured: { [x: string]: JsonValue } content: Array } } @@ -1821,7 +1821,7 @@ export type SessionToolProgress = { export type SessionToolSuccess = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.tool.success" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -1829,9 +1829,9 @@ export type SessionToolSuccess = { sessionID: string assistantMessageID: string callID: string - structured: { [x: string]: unknown } + structured: { [x: string]: JsonValue } content: Array - result?: unknown + result?: JsonValue executed: boolean resultState?: SessionMessageProviderState6 } @@ -1881,7 +1881,7 @@ export type FormState = { status: "pending" } | { status: "answered"; answer: Fo export type FormReplied = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "form.replied" location?: LocationRef data: { id: string; sessionID: string; answer: FormAnswer } @@ -1907,7 +1907,7 @@ export type FilePartSource = FileSource | SymbolSource | ResourceSource export type QuestionV2Asked = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.v2.asked" location?: LocationRef data: { id: string; sessionID: string; questions: Array; tool?: QuestionV2Tool } @@ -1931,12 +1931,20 @@ export type FormField1 = export type QuestionAsked = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "question.asked" location?: LocationRef data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool | undefined } } +export type ReferenceInfo = { + name: string + path: string + description?: string + hidden?: boolean + source: ReferenceSource +} + export type AgentInfo = { id: string name: string @@ -2041,12 +2049,19 @@ export type SessionMessageAssistant = { retry?: SessionMessageAssistantRetry } +export type IntegrationInfo = { + id: string + name: string + methods: Array + connections: Array +} + export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } export type SessionCreated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.created" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -2056,7 +2071,7 @@ export type SessionCreated = { export type SessionUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -2066,7 +2081,7 @@ export type SessionUpdated = { export type SessionDeleted1 = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.deleted" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -2076,7 +2091,7 @@ export type SessionDeleted1 = { export type MessageUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "message.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -2098,7 +2113,7 @@ export type FormInfo1 = { id: string; sessionID: string; title: string; metadata export type SessionInputAdmitted = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "session.input.admitted" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -2121,7 +2136,7 @@ export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted export type FormCreated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "form.created" location?: LocationRef data: { form: FormInfo1 } @@ -2202,7 +2217,7 @@ export type Part = export type MessagePartUpdated = { id: string created: number - metadata?: { [x: string]: unknown } + metadata?: { [x: string]: JsonValue } type: "message.part.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef @@ -3241,7 +3256,7 @@ export type IntegrationListInput = { export type IntegrationListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array<{ id: string; name: string; methods: Array; connections: Array }> + data: Array } export type IntegrationGetInput = { @@ -3253,7 +3268,7 @@ export type IntegrationGetInput = { export type IntegrationGetOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: { id: string; name: string; methods: Array; connections: Array } | null + data: IntegrationInfo | null } export type IntegrationConnectKeyInput = { @@ -4539,7 +4554,7 @@ export type ShellListInput = { export type ShellListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type ShellCreateInput = { @@ -4574,7 +4589,7 @@ export type ShellCreateInput = { export type ShellCreateOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Shell1 + data: ShellInfo1 } export type ShellGetInput = { @@ -4586,7 +4601,7 @@ export type ShellGetInput = { export type ShellGetOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Shell1 + data: ShellInfo1 } export type ShellTimeoutInput = { @@ -4599,7 +4614,7 @@ export type ShellTimeoutInput = { export type ShellTimeoutOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Shell1 + data: ShellInfo1 } export type ShellOutputInput = { @@ -4673,7 +4688,7 @@ export type ReferenceListInput = { export type ReferenceListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array<{ name: string; path: string; description?: string; hidden?: boolean; source: ReferenceSource }> + data: Array } export type ProjectCopyCreateInput = { diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 4e57d390d1..236d027f3f 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -303,7 +303,7 @@ const layer = Layer.effect( } const project = (entry: Entry, connections: IntegrationConnection.Info[]) => - new Info({ + Info.make({ id: entry.ref.id, name: entry.ref.name, methods: entry.methods, diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index fe9e48e1da..3c0e7bb479 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -64,7 +64,7 @@ const layer = Layer.effect( if (source.type === "local") { materialized.set( name, - new Info({ + Info.make({ name, path: source.path, ...(source.description === undefined ? {} : { description: source.description }), @@ -88,7 +88,7 @@ const layer = Layer.effect( seen.set(target, source.branch) materialized.set( name, - new Info({ + Info.make({ name, path: AbsolutePath.make(target), ...(source.description === undefined ? {} : { description: source.description }), diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index dc20303f00..0cbf46bb44 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -40,7 +40,7 @@ describe("Integration", () => { .transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI"))) .pipe(Scope.provide(scope)) expect(yield* integrations.get(openai)).toEqual( - new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }), + Integration.Info.make({ id: openai, name: "OpenAI", methods: [], connections: [] }), ) yield* Scope.close(scope, Exit.void) diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index bb8e460d73..096d73f5bf 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -214,7 +214,7 @@ describe("ModelsDevPlugin", () => { }), ) expect(yield* integrations.list()).toEqual([ - new Integration.Info({ + Integration.Info.make({ id: Integration.ID.make("acme"), name: "Acme", methods: [ diff --git a/packages/core/test/reference-guidance.test.ts b/packages/core/test/reference-guidance.test.ts index 17d412f902..cdd3b36898 100644 --- a/packages/core/test/reference-guidance.test.ts +++ b/packages/core/test/reference-guidance.test.ts @@ -26,7 +26,7 @@ describe("ReferenceGuidance", () => { Layer.mock(Reference.Service, { list: () => Effect.succeed([ - new Reference.Info({ + Reference.Info.make({ name: "docs", path: AbsolutePath.make("/docs"), description: "Use for product documentation", @@ -62,7 +62,7 @@ describe("ReferenceGuidance", () => { Layer.mock(Reference.Service, { list: () => Effect.succeed([ - new Reference.Info({ + Reference.Info.make({ name: "docs", path: AbsolutePath.make("/docs"), source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }), @@ -76,7 +76,7 @@ describe("ReferenceGuidance", () => { it.effect("announces added and removed references as deltas", () => { const reference = (name: string, description: string) => - new Reference.Info({ + Reference.Info.make({ name, path: AbsolutePath.make(`/${name}`), description, diff --git a/packages/core/test/reference.test.ts b/packages/core/test/reference.test.ts index db61232310..a5a656c8bf 100644 --- a/packages/core/test/reference.test.ts +++ b/packages/core/test/reference.test.ts @@ -29,7 +29,7 @@ describe("Reference", () => { yield* references.transform((editor) => editor.add("docs", source)).pipe(Scope.provide(scope)) expect(yield* references.list()).toEqual([ - new Reference.Info({ name: "docs", path, description: "Use for API documentation", hidden: true, source }), + Reference.Info.make({ name: "docs", path, description: "Use for API documentation", hidden: true, source }), ]) yield* Scope.close(scope, Exit.void) @@ -45,7 +45,7 @@ describe("Reference", () => { yield* references.transform((editor) => editor.add("sdk", source)) expect(yield* references.list()).toEqual([ - new Reference.Info({ + Reference.Info.make({ name: "sdk", path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), source, @@ -66,7 +66,7 @@ describe("Reference", () => { yield* references.transform((editor) => editor.add("sdk", source)) expect(yield* references.list()).toEqual([ - new Reference.Info({ + Reference.Info.make({ name: "sdk", path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), description: "Use for SDK implementation details", diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 3cb1a69633..3a70f61e4c 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -824,7 +824,10 @@ function structuralTypes(schemas: ReadonlyArray, mutable: boolean, r const pattern = `(?/g, "").replaceAll("Schema.Json", "JsonValue") + const output = type + .replaceAll(/ & Brand\.Brand<"[^"]+">/g, "") + .replaceAll("Schema.Json", "JsonValue") + .replaceAll(/(?("Integration.Info")({ +export const Info = Schema.Struct({ id: ID, name: Schema.String, methods: Schema.Array(Method), connections: Schema.Array(Connection.Info), -}) {} +}).annotate({ identifier: "Integration.Info" }) +export interface Info extends Schema.Schema.Type {} export const AttemptID = Schema.String.pipe( Schema.brand("Integration.AttemptID"), diff --git a/packages/schema/src/reference.ts b/packages/schema/src/reference.ts index 9dd277f62b..0b4311a93b 100644 --- a/packages/schema/src/reference.ts +++ b/packages/schema/src/reference.ts @@ -30,10 +30,11 @@ export const Source = Schema.Union([LocalSource, GitSource]) .annotate({ identifier: "Reference.Source" }) export type Source = typeof Source.Type -export class Info extends Schema.Class("Reference.Info")({ +export const Info = Schema.Struct({ name: Schema.String, path: AbsolutePath, description: Schema.String.pipe(optional), hidden: Schema.Boolean.pipe(optional), source: Source, -}) {} +}).annotate({ identifier: "Reference.Info" }) +export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/src/shell.ts b/packages/schema/src/shell.ts index 59ac25decf..bcd09cd901 100644 --- a/packages/schema/src/shell.ts +++ b/packages/schema/src/shell.ts @@ -23,8 +23,8 @@ export const Status = Schema.Literals(["running", "exited", "timeout", "killed"] export type Status = typeof Status.Type export const Time = Schema.Struct({ - started: Schema.Number, - completed: optional(Schema.Number), + started: Schema.Finite, + completed: optional(Schema.Finite), }) export interface Time extends Schema.Schema.Type {} @@ -42,15 +42,15 @@ export const Info = Schema.Struct({ // Absolute path of the file capturing combined stdout/stderr. Page through it via `output`. file: Schema.String, pid: optional(NonNegativeInt), - exit: optional(Schema.Number), + exit: optional(Schema.Finite), // Always present; defaults to an empty object when the creator supplies no metadata. metadata: Metadata, time: Time, -}).annotate({ identifier: "Shell" }) +}).annotate({ identifier: "Shell.Info" }) export interface Info extends Schema.Schema.Type {} const Created = ephemeral({ type: "shell.created", schema: { info: Info } }) -const Exited = ephemeral({ type: "shell.exited", schema: { id: ID, exit: optional(Schema.Number), status: Status } }) +const Exited = ephemeral({ type: "shell.exited", schema: { id: ID, exit: optional(Schema.Finite), status: Status } }) const Deleted = ephemeral({ type: "shell.deleted", schema: { id: ID } }) export const Event = { Created, Exited, Deleted, Definitions: inventory(Created, Exited, Deleted) } diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index fd6e4ea4de..c66fa690ce 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -53,8 +53,6 @@ import { DialogThemeList } from "./component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { DialogAgent } from "./component/dialog-agent" import { DialogSessionList } from "./component/dialog-session-list" -import { DialogWorkspaceList } from "./component/dialog-workspace-list" -import { DialogConsoleOrg } from "./component/dialog-console-org" import { ThemeProvider, useTheme } from "./context/theme" import { Home } from "./routes/home" import { Session } from "./routes/session" @@ -121,7 +119,6 @@ const appBindingCommands = [ "variant.cycle", "variant.list", "provider.connect", - "console.org.switch", "opencode.status", "server.pair", "opencode.debug", @@ -131,7 +128,6 @@ const appBindingCommands = [ "help.show", "docs.open", "diff.open", - "workspace.list", "app.debug", "app.console", "app.heap_snapshot", @@ -618,13 +614,6 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi }) const connected = useConnected() - const currentWorktreeWorkspace = createMemo(() => { - const workspaceID = project.workspace.current() - if (!workspaceID) return - const workspace = project.workspace.get(workspaceID) - if (workspace?.type !== "worktree" || !workspace.directory) return - return workspace - }) const appCommands = createMemo(() => [ { @@ -661,31 +650,6 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi dialog.clear() }, }, - { - name: "workspace.copy_path", - title: "Copy worktree path", - category: "Workspace", - enabled: () => currentWorktreeWorkspace() !== undefined, - run: async () => { - const workspace = currentWorktreeWorkspace() - if (!workspace?.directory) return - await clipboard - .write?.(workspace.directory) - .then(() => toast.show({ message: "Copied worktree path", variant: "info" })) - .catch(toast.error) - dialog.clear() - }, - }, - { - name: "workspace.list", - title: "Manage workspaces", - category: "Workspace", - hidden: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, - slashName: "workspaces", - run: () => { - dialog.replace(() => ) - }, - }, ...Array.from({ length: 9 }, (_, i) => ({ name: `session.quick_switch.${i + 1}`, title: `Switch to session in quick slot ${i + 1}`, @@ -818,21 +782,6 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi }, category: "Integration", }, - ...(sync.data.console_state.switchableOrgCount > 1 - ? [ - { - name: "console.org.switch", - title: "Switch org", - suggested: Boolean(sync.data.console_state.activeOrgName), - slashName: "org", - slashAliases: ["orgs", "switch-org"], - run: () => { - dialog.replace(() => ) - }, - category: "Provider", - }, - ] - : []), { name: "opencode.status", title: "View status", @@ -1040,7 +989,6 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi category: "System", run: async () => { kv.set("session_directory_filter_enabled", !kv.get("session_directory_filter_enabled", true)) - await sync.session.refresh() dialog.clear() }, }, diff --git a/packages/tui/src/component/dialog-console-org.tsx b/packages/tui/src/component/dialog-console-org.tsx deleted file mode 100644 index 1305a965cb..0000000000 --- a/packages/tui/src/component/dialog-console-org.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { createResource, createMemo, createSignal } from "solid-js" -import { TextAttributes } from "@opentui/core" -import { DialogSelect } from "../ui/dialog-select" -import { useSDK } from "../context/sdk" -import { useDialog } from "../ui/dialog" -import { useToast } from "../ui/toast" -import { useTheme } from "../context/theme" -import { errorMessage } from "../util/error" -import type { ExperimentalConsoleListOrgsResponse } from "@opencode-ai/sdk/v2" - -type OrgOption = ExperimentalConsoleListOrgsResponse["orgs"][number] - -const accountHost = (url: string) => { - try { - return new URL(url).host - } catch { - return url - } -} - -const accountLabel = (item: Pick) => - `${item.accountEmail} ${accountHost(item.accountUrl)}` - -export function DialogConsoleOrg() { - const sdk = useSDK() - const dialog = useDialog() - const toast = useToast() - const { theme } = useTheme() - - const [loadError, setLoadError] = createSignal() - - const [orgs] = createResource(() => - sdk.client.experimental.console - .listOrgs({}, { throwOnError: true }) - .then((result) => result.data?.orgs ?? []) - // Catch so the rejected resource never reaches the memos below: reading - // orgs() in an errored state re-throws and tears down the dialog. - .catch((error) => { - setLoadError(error) - return undefined - }), - ) - - const showError = createMemo(() => Boolean(loadError())) - - const current = createMemo(() => orgs()?.find((item) => item.active)) - - const options = createMemo(() => { - if (showError()) return [] - const listed = orgs() - if (listed === undefined) { - return [ - { - title: "Loading orgs...", - value: "loading", - onSelect: () => {}, - }, - ] - } - - if (listed.length === 0) { - return [ - { - title: "No orgs found", - value: "empty", - onSelect: () => {}, - }, - ] - } - - return listed - .toSorted((a, b) => { - const activeAccountA = a.active ? 0 : 1 - const activeAccountB = b.active ? 0 : 1 - if (activeAccountA !== activeAccountB) return activeAccountA - activeAccountB - - const accountCompare = accountLabel(a).localeCompare(accountLabel(b)) - if (accountCompare !== 0) return accountCompare - - return a.orgName.localeCompare(b.orgName) - }) - .map((item) => ({ - title: item.orgName, - value: item, - category: accountLabel(item), - categoryView: ( - - {item.accountEmail} - {accountHost(item.accountUrl)} - - ), - onSelect: async () => { - if (item.active) { - dialog.clear() - return - } - - await sdk.client.experimental.console.switchOrg( - { - accountID: item.accountID, - orgID: item.orgID, - }, - { throwOnError: true }, - ) - - await sdk.client.instance.dispose() - toast.show({ - message: `Switched to ${item.orgName}`, - variant: "info", - }) - dialog.clear() - }, - })) - }) - - return ( - - title="Switch org" - options={options()} - current={current()} - renderFilter={!showError()} - locked={showError()} - emptyView={ - showError() ? ( - - - Could not load orgs - - {errorMessage(loadError())} - - ) : undefined - } - /> - ) -} diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 1b962142fc..1ae8bbec77 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -1,6 +1,10 @@ import { TextAttributes } from "@opentui/core" -import type { IntegrationConnectOauthOutput } from "@opencode-ai/client/promise" -import type { ConnectionInfo, IntegrationInfo, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2" +import type { + ConnectionInfo, + IntegrationConnectOauthOutput, + IntegrationInfo, + IntegrationOAuthMethod, +} from "@opencode-ai/client" import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" import { useClipboard } from "../context/clipboard" import { useData } from "../context/data" diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 003c207be0..5702759159 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -5,7 +5,7 @@ import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useTheme, type Theme } from "../context/theme" import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" -import type { McpServer } from "@opencode-ai/sdk/v2" +import type { McpServer } from "@opencode-ai/client" import { useClipboard } from "../context/clipboard" import { useToast } from "../ui/toast" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx index e0beff09ed..cf6cb361de 100644 --- a/packages/tui/src/component/dialog-move-session.tsx +++ b/packages/tui/src/component/dialog-move-session.tsx @@ -6,7 +6,7 @@ import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useSDK } from "../context/sdk" import { useTheme } from "../context/theme" -import { useSync } from "../context/sync" +import { useData } from "../context/data" import { abbreviateHome } from "../runtime" import { useTuiPaths } from "../context/runtime" import { Locale } from "../util/locale" @@ -17,7 +17,7 @@ import { useCommandShortcut } from "../keymap" import { useProject } from "../context/project" import { Spinner } from "./spinner" import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes" -import type { ProjectDirectoriesOutput } from "@opencode-ai/client/promise" +import type { ProjectDirectoriesOutput } from "@opencode-ai/client" import { useRoute } from "../context/route" import { DialogProjectCopyName } from "./dialog-project-copy-name" @@ -38,7 +38,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { const sdk = useSDK() const dimensions = useTerminalDimensions() const { theme } = useTheme() - const sync = useSync() + const sessionData = useData() const projectContext = useProject() const route = useRoute() const toast = useToast() @@ -132,9 +132,13 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { }) if (roots.length === 0) return [{ title: "No project directories found", value: undefined }] - const subdirectories = sync.data.session - .filter((session) => session.projectID === props.projectID && session.path && ![".", "/"].includes(session.path)) - .map((session) => session.directory) + const subdirectories = sessionData.session + .list() + .filter( + (session) => + session.projectID === props.projectID && session.subpath && ![".", "/"].includes(session.subpath), + ) + .map((session) => session.location.directory) .filter((directory) => !roots.some((root) => root.directory === directory)) .filter((directory, index, directories) => directories.indexOf(directory) === index) .map((location) => ({ diff --git a/packages/tui/src/component/dialog-provider.tsx b/packages/tui/src/component/dialog-provider.tsx deleted file mode 100644 index 0fd51e3c1c..0000000000 --- a/packages/tui/src/component/dialog-provider.tsx +++ /dev/null @@ -1,469 +0,0 @@ -import { createMemo, createSignal, onMount, Show } from "solid-js" -import { useSync } from "../context/sync" -import { map, pipe, sortBy } from "remeda" -import { DialogSelect } from "../ui/dialog-select" -import { useDialog } from "../ui/dialog" -import { useSDK } from "../context/sdk" -import { DialogPrompt } from "../ui/dialog-prompt" -import { Link } from "../ui/link" -import { useTheme } from "../context/theme" -import { TextAttributes } from "@opentui/core" -import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2" -import { DialogModel } from "./dialog-model" -import { useToast } from "../ui/toast" -import { isConsoleManagedProvider } from "../util/provider-origin" -import { useConnected } from "./use-connected" -import { useBindings } from "../keymap" -import { useClipboard } from "../context/clipboard" - -const PROVIDER_PRIORITY: Record = { - opencode: 0, - "opencode-go": 1, - openai: 2, - "github-copilot": 3, - anthropic: 4, - google: 5, -} - -const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__" -const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ - -type ProviderOptionBase = { - title: string - value: string - description?: string - category: string -} - -type ProviderOption = - | (ProviderOptionBase & { - type: "provider" - providerID: string - }) - | (ProviderOptionBase & { - type: "custom" - }) - -export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] { - return [ - ...pipe( - list, - sortBy( - (x) => PROVIDER_PRIORITY[x.id] ?? 99, - (x) => x.name.toLowerCase(), - (x) => x.id, - ), - map((provider) => ({ - type: "provider" as const, - title: provider.name, - value: provider.id, - providerID: provider.id, - description: { - opencode: "(Recommended)", - anthropic: "(API key)", - openai: "(ChatGPT Plus/Pro or API key)", - "opencode-go": "Low cost subscription for everyone", - }[provider.id], - category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers", - })), - ), - { - type: "custom", - title: "Other", - value: CUSTOM_PROVIDER_OPTION_VALUE, - description: "Custom provider", - category: "Providers", - }, - ] -} - -export function normalizeCustomProviderID(value: string) { - const providerID = value.trim().replace(/^@ai-sdk\//, "") - if (!CUSTOM_PROVIDER_ID.test(providerID)) return - return providerID -} - -export function createDialogProviderOptions() { - const sync = useSync() - const dialog = useDialog() - const sdk = useSDK() - const toast = useToast() - const { theme } = useTheme() - const onboarded = useConnected() - - async function promptCustomProviderID(): Promise { - const value = await DialogPrompt.show(dialog, "Other", { - placeholder: "Provider id", - description: () => ( - - This only stores a credential. Configure the provider in opencode.json to use it. - - ), - }) - if (value === null) return - - const providerID = normalizeCustomProviderID(value) - if (providerID) return providerID - - toast.show({ - variant: "error", - message: - "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", - }) - return promptCustomProviderID() - } - - const options = createMemo(() => { - return pipe( - providerOptions(sync.data.provider_next.all), - map((provider) => { - if (provider.type === "custom") { - return { - title: provider.title, - value: provider.value, - description: provider.description, - category: provider.category, - async onSelect() { - const providerID = await promptCustomProviderID() - if (!providerID) return - return dialog.replace(() => ) - }, - } - } - - const providerID = provider.providerID - const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID) - const connected = sync.data.provider_next.connected.includes(providerID) - - return { - title: provider.title, - value: provider.value, - description: provider.description, - footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined, - category: provider.category, - gutter: connected && onboarded() ? () => : undefined, - async onSelect() { - if (consoleManaged) return - - const methods = sync.data.provider_auth[providerID] ?? [ - { - type: "api", - label: "API key", - }, - ] - let index: number | null = 0 - if (methods.length > 1) { - index = await new Promise((resolve) => { - dialog.replace( - () => ( - ({ - title: x.label, - value: index, - }))} - onSelect={(option) => resolve(option.value)} - /> - ), - () => resolve(null), - ) - }) - } - if (index == null) return - const method = methods[index] - if (method.type === "oauth") { - let inputs: Record | undefined - if (method.prompts?.length) { - const value = await PromptsMethod({ - dialog, - prompts: method.prompts, - }) - if (!value) return - inputs = value - } - - const result = await sdk.client.provider.oauth.authorize({ - providerID, - method: index, - inputs, - }) - if (result.error) { - toast.show({ - variant: "error", - message: JSON.stringify(result.error), - }) - dialog.clear() - return - } - if (result.data?.method === "code") { - dialog.replace(() => ( - - )) - } - if (result.data?.method === "auto") { - dialog.replace(() => ( - - )) - } - } - if (method.type === "api") { - let metadata: Record | undefined - if (method.prompts?.length) { - const value = await PromptsMethod({ dialog, prompts: method.prompts }) - if (!value) return - metadata = value - } - return dialog.replace(() => ( - - )) - } - }, - } - }), - ) - }) - return options -} - -export function DialogProvider() { - const options = createDialogProviderOptions() - return -} - -interface AutoMethodProps { - index: number - providerID: string - title: string - authorization: ProviderAuthAuthorization -} -function AutoMethod(props: AutoMethodProps) { - const { theme } = useTheme() - const sdk = useSDK() - const dialog = useDialog() - const sync = useSync() - const toast = useToast() - const clipboard = useClipboard() - - useBindings(() => ({ - bindings: [ - { - key: "c", - desc: "Copy provider code", - group: "Dialog", - cmd: () => { - const code = - props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url - clipboard - .write?.(code) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) - }, - }, - ], - })) - - onMount(async () => { - const result = await sdk.client.provider.oauth.callback({ - providerID: props.providerID, - method: props.index, - }) - if (result.error) { - toast.show({ - variant: "error", - message: - "name" in result.error && result.error.name === "ProviderAuthOauthCallbackFailed" - ? "OAuth authorization failed. Try /connect again." - : JSON.stringify(result.error), - }) - dialog.clear() - return - } - await sdk.client.instance.dispose() - await sync.bootstrap() - dialog.replace(() => ) - }) - - return ( - - - - {props.title} - - dialog.clear()}> - esc - - - - - {props.authorization.instructions} - - Waiting for authorization... - - c copy - - - ) -} - -interface CodeMethodProps { - index: number - title: string - providerID: string - authorization: ProviderAuthAuthorization -} -function CodeMethod(props: CodeMethodProps) { - const { theme } = useTheme() - const sdk = useSDK() - const sync = useSync() - const dialog = useDialog() - const [error, setError] = createSignal(false) - - return ( - { - const { error } = await sdk.client.provider.oauth.callback({ - providerID: props.providerID, - method: props.index, - code: value, - }) - if (!error) { - await sdk.client.instance.dispose() - await sync.bootstrap() - dialog.replace(() => ) - return - } - setError(true) - }} - description={() => ( - - {props.authorization.instructions} - - - Invalid code - - - )} - /> - ) -} - -interface ApiMethodProps { - providerID: string - title: string - metadata?: Record - custom?: boolean -} -function ApiMethod(props: ApiMethodProps) { - const dialog = useDialog() - const sdk = useSDK() - const sync = useSync() - const toast = useToast() - const { theme } = useTheme() - - return ( - - ({ - opencode: ( - - - OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API - key. - - - Go to https://opencode.ai/zen to get a key - - - ), - "opencode-go": ( - - - OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models - with generous usage limits. - - - Go to https://opencode.ai/go and enable OpenCode Go - - - ), - })[props.providerID] ?? undefined - } - onConfirm={async (value) => { - if (!value) return - await sdk.client.auth.set({ - providerID: props.providerID, - auth: { - type: "api", - key: value, - ...(props.metadata ? { metadata: props.metadata } : {}), - }, - }) - await sdk.client.instance.dispose() - await sync.bootstrap() - if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) { - toast.show({ - variant: "info", - message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`, - }) - dialog.clear() - return - } - dialog.replace(() => ) - }} - /> - ) -} - -interface PromptsMethodProps { - dialog: ReturnType - prompts: NonNullable[number][] -} -async function PromptsMethod(props: PromptsMethodProps) { - const inputs: Record = {} - for (const prompt of props.prompts) { - if (prompt.when) { - const value = inputs[prompt.when.key] - if (value === undefined) continue - const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value - if (!matches) continue - } - - if (prompt.type === "select") { - const value = await new Promise((resolve) => { - props.dialog.replace( - () => ( - ({ - title: x.label, - value: x.value, - description: x.hint, - }))} - onSelect={(option) => resolve(option.value)} - /> - ), - () => resolve(null), - ) - }) - if (value === null) return null - inputs[prompt.key] = value - continue - } - - const value = await new Promise((resolve) => { - props.dialog.replace( - () => ( - resolve(value)} /> - ), - () => resolve(null), - ) - }) - if (value === null) return null - inputs[prompt.key] = value - } - return inputs -} diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 8fae5114ab..2d305f54e6 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -1,6 +1,6 @@ import { createMemo, createResource, createSignal, onMount } from "solid-js" import path from "path" -import type { SessionInfo } from "@opencode-ai/sdk/v2" +import type { SessionInfo } from "@opencode-ai/client" import { useDialog } from "../ui/dialog" import { DialogSelect } from "../ui/dialog-select" import { useRoute } from "../context/route" diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx index bb54dd8713..c8e493ef37 100644 --- a/packages/tui/src/component/dialog-skill.tsx +++ b/packages/tui/src/component/dialog-skill.tsx @@ -5,7 +5,7 @@ import { useDialog } from "../ui/dialog" import { useTheme } from "../context/theme" import { errorMessage } from "../util/error" import { useData } from "../context/data" -import type { LocationRef } from "@opencode-ai/sdk/v2" +import type { LocationRef } from "@opencode-ai/client" export type DialogSkillProps = { location?: LocationRef diff --git a/packages/tui/src/component/dialog-workspace-create.tsx b/packages/tui/src/component/dialog-workspace-create.tsx deleted file mode 100644 index 23e4a8df3a..0000000000 --- a/packages/tui/src/component/dialog-workspace-create.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import type { ExperimentalWorkspaceAdapterListResponse, Workspace } from "@opencode-ai/sdk/v2" -import { useDialog } from "../ui/dialog" -import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select" -import { useSync } from "../context/sync" -import { useProject } from "../context/project" -import { useRoute } from "../context/route" -import { createMemo, createSignal, onMount } from "solid-js" -import { errorMessage } from "../util/error" -import { useSDK } from "../context/sdk" -import { useToast } from "../ui/toast" -import { DialogAlert } from "../ui/dialog-alert" -import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes" - -type Adapter = ExperimentalWorkspaceAdapterListResponse[number] - -export type WorkspaceSelection = - | { - type: "none" - } - | { - type: "new" - workspaceType: string - workspaceName: string - } - | { - type: "existing" - workspaceID: string - workspaceType: string - workspaceName: string - } - -type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } -type ExistingWorkspaceSelectValue = { workspace: Workspace } - -export function recentConnectedWorkspaces(input: { - workspaces: readonly WorkspaceInfo[] - status: (workspaceID: string) => string | undefined - limit?: number - omitWorkspaceID?: string -}) { - const allWorkspaces = input.workspaces.filter((workspace) => input.status(workspace.id) === "connected") - const workspaces = allWorkspaces.toSorted((a, b) => Number(b.timeUsed) - Number(a.timeUsed)) - const recent = workspaces.slice(0, input.limit ?? 3) - - return { recent, hasMore: recent.length < workspaces.length } -} - -export function warpReminderText(dir: string) { - return `The user has changed the current working directory to "${dir}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.` -} - -async function loadWorkspaceAdapters(input: { - sdk: ReturnType - sync: ReturnType - toast: ReturnType -}) { - const dir = input.sync.path.directory || process.cwd() - try { - const response = await input.sdk.client.experimental.workspace.adapter.list({ directory: dir }) - if (response.error) throw response.error - return response.data - } catch (err) { - input.toast.show({ - title: "Failed to load workspace adapters", - message: errorMessage(err), - variant: "error", - }) - return undefined - } -} - -export async function openWorkspaceSelect(input: { - dialog: ReturnType - sdk: ReturnType - sync: ReturnType - project: ReturnType - toast: ReturnType - onSelect: (selection: WorkspaceSelection) => Promise | void -}) { - input.dialog.clear() - await input.sdk.client.experimental.workspace.syncList().catch(() => undefined) - await input.project.workspace.sync().catch(() => undefined) - const adapters = await loadWorkspaceAdapters(input) - if (!adapters) return - input.dialog.replace(() => ) -} - -export async function warpWorkspaceSession(input: { - dialog: ReturnType - sdk: ReturnType - sync: ReturnType - project: ReturnType - toast: ReturnType - sourceWorkspaceID?: string - workspaceID: string | null - sessionID: string - copyChanges: boolean - done?: () => void -}): Promise { - let result - try { - result = await input.sdk.client.experimental.workspace.warp({ - id: input.workspaceID, - sessionID: input.sessionID, - copyChanges: input.copyChanges, - }) - } catch (err) { - input.toast.show({ - title: "Failed to warp session", - message: errorMessage(err), - variant: "error", - }) - return false - } - if (!result?.data) { - if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") { - await DialogAlert.show( - input.dialog, - "Unable to Warp Session", - "Unable to apply file changes to this workspace. It has existing changes that conflict or is based off a different branch. Session has not been warped.", - ) - return false - } - - input.toast.show({ - title: "Failed to warp session", - message: errorMessage(result?.error ?? "no response"), - variant: "error", - }) - return false - } - - input.project.workspace.set(input.workspaceID) - - await input.sync.bootstrap({ fatal: false }).catch(() => undefined) - - const dir = input.project.instance.directory() || input.sync.path.directory - if (dir) { - await input.sdk.client.session - .promptAsync({ - sessionID: input.sessionID, - workspace: input.workspaceID ?? undefined, - noReply: true, - parts: [ - { - type: "text", - text: warpReminderText(dir), - synthetic: true, - }, - ], - }) - .catch(() => undefined) - } - - await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]) - - if (input.done) { - input.done() - return true - } - input.dialog.clear() - return true -} - -export async function confirmWorkspaceFileChanges(input: { - dialog: ReturnType - sdk: ReturnType - sourceWorkspaceID?: string -}) { - const status = await input.sdk.client.vcs.status({ workspace: input.sourceWorkspaceID }).catch(() => undefined) - const fileChangeChoice = status?.data?.length - ? await DialogWorkspaceFileChanges.show(input.dialog, status.data) - : "no" - if (!fileChangeChoice) return - return fileChangeChoice === "yes" -} - -export function DialogWorkspaceSelect(props: { - adapters?: Adapter[] - onSelect: (selection: WorkspaceSelection) => Promise | void -}) { - const dialog = useDialog() - const project = useProject() - const route = useRoute() - const sync = useSync() - const sdk = useSDK() - const toast = useToast() - const [adapters, setAdapters] = createSignal(props.adapters) - const omittedWorkspaceID = createMemo(() => (route.data.type === "session" ? project.workspace.current() : undefined)) - - onMount(() => { - dialog.setSize("medium") - void (async () => { - if (adapters()) return - const res = await loadWorkspaceAdapters({ sdk, sync, toast }) - if (!res) return - setAdapters(res) - })() - }) - - const options = createMemo[]>(() => { - const list = adapters() - if (!list) return [] - const { recent, hasMore } = recentConnectedWorkspaces({ - workspaces: project.workspace.list(), - status: project.workspace.status, - omitWorkspaceID: omittedWorkspaceID(), - }) - return [ - ...list.map((adapter) => ({ - title: adapter.name, - value: { type: "new" as const, workspaceType: adapter.type, workspaceName: adapter.name }, - description: adapter.description, - category: "New workspace", - })), - { - title: "None", - value: { type: "none" as const }, - description: "Use the local project", - category: "Choose workspace", - }, - ...recent.map((workspace: Workspace) => ({ - title: workspace.name, - description: `(${workspace.type})`, - value: { - type: "existing" as const, - workspaceID: workspace.id, - workspaceType: workspace.type, - workspaceName: workspace.name, - }, - category: "Choose workspace", - })), - ...(hasMore - ? [ - { - title: "View all workspaces", - value: { type: "existing-list" as const }, - description: "Choose from all workspaces", - category: "Choose workspace", - }, - ] - : []), - ] - }) - - if (!adapters()) return null - return ( - - title="Warp" - skipFilter={true} - renderFilter={false} - options={options()} - onSelect={(option) => { - if (!option.value) return - if (option.value.type === "none") { - void props.onSelect(option.value) - return - } - if (option.value.type === "new") { - void props.onSelect(option.value) - return - } - if (option.value.type === "existing") { - void props.onSelect(option.value) - return - } - - dialog.replace(() => ( - - )) - }} - /> - ) -} - -function DialogExistingWorkspaceSelect(props: { - omitWorkspaceID?: string - onSelect: (selection: WorkspaceSelection) => Promise | void -}) { - const project = useProject() - - const options = createMemo[]>(() => - project.workspace - .list() - .filter((workspace) => project.workspace.status(workspace.id) === "connected") - .filter((workspace) => workspace.id !== props.omitWorkspaceID) - .map((workspace: Workspace) => ({ - title: workspace.name, - description: `(${workspace.type})`, - value: { workspace }, - })), - ) - - return ( - - title="Existing Workspace" - options={options()} - onSelect={(option) => { - void props.onSelect({ - type: "existing", - workspaceID: option.value.workspace.id, - workspaceType: option.value.workspace.type, - workspaceName: option.value.workspace.name, - }) - }} - /> - ) -} diff --git a/packages/tui/src/component/dialog-workspace-file-changes.tsx b/packages/tui/src/component/dialog-workspace-file-changes.tsx index 8918a2e7e8..bca389c368 100644 --- a/packages/tui/src/component/dialog-workspace-file-changes.tsx +++ b/packages/tui/src/component/dialog-workspace-file-changes.tsx @@ -1,6 +1,6 @@ import { TextAttributes } from "@opentui/core" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" -import type { VcsFileStatus } from "@opencode-ai/sdk/v2" +import type { VcsFileStatus } from "@opencode-ai/client" import { createMemo, For } from "solid-js" import { createStore } from "solid-js/store" import { FilePath } from "../ui/file-path" diff --git a/packages/tui/src/component/dialog-workspace-list.tsx b/packages/tui/src/component/dialog-workspace-list.tsx deleted file mode 100644 index eab2acf7c8..0000000000 --- a/packages/tui/src/component/dialog-workspace-list.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import type { Workspace } from "@opencode-ai/sdk/v2" -import { useDialog } from "../ui/dialog" -import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select" -import { useProject } from "../context/project" -import { useRoute } from "../context/route" -import { useSync } from "../context/sync" -import { useTheme } from "../context/theme" -import { createMemo, createSignal, onMount } from "solid-js" -import { createStore } from "solid-js/store" -import { errorMessage } from "../util/error" -import { useSDK } from "../context/sdk" -import { useToast } from "../ui/toast" - -type WorkspaceOption = { workspace: Workspace } - -export function DialogWorkspaceList() { - const dialog = useDialog() - const route = useRoute() - const sync = useSync() - const sdk = useSDK() - const toast = useToast() - const project = useProject() - const { theme } = useTheme() - const [deleting, setDeleting] = createSignal() - const [removing, setRemoving] = createSignal() - const [expanded, setExpanded] = createStore>({}) - - const current = createMemo(() => { - if (route.data.type === "session") return sync.session.get(route.data.sessionID)?.workspaceID - return project.workspace.current() - }) - - const options = createMemo[]>(() => - project.workspace - .list() - .toSorted((a, b) => a.name.localeCompare(b.name)) - .map((workspace) => { - const status = project.workspace.status(workspace.id) - return { - title: - removing() === workspace.id - ? "Deleting..." - : deleting() === workspace.id - ? `Delete ${workspace.name}? Press delete again` - : workspace.name, - value: { workspace }, - footer: workspace.type, - details: expanded[workspace.id] && workspace.directory ? [workspace.directory] : undefined, - gutter: () => , - } - }), - ) - - function showDetails(workspace: Workspace) { - setExpanded(workspace.id, (open) => !open) - } - - async function remove(workspace: Workspace) { - if (removing()) return - if (deleting() !== workspace.id) { - setDeleting(workspace.id) - return - } - - setDeleting(undefined) - setRemoving(workspace.id) - const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({ - error: err, - })) - if (result?.error) { - setRemoving(undefined) - toast.show({ - variant: "error", - title: "Failed to delete workspace", - message: errorMessage(result.error), - }) - return - } - - if (current() === workspace.id) { - project.workspace.set(undefined) - route.navigate({ type: "home" }) - } - await project.workspace.sync() - await sync.bootstrap({ fatal: false }).catch(() => undefined) - setRemoving(undefined) - } - - onMount(() => { - dialog.setSize("large") - void sdk.client.experimental.workspace.syncList().catch(() => undefined) - void project.workspace.sync() - }) - - return ( - { - setDeleting(undefined) - }} - onSelect={(option) => showDetails(option.value.workspace)} - actions={[ - { - command: "session.delete", - title: "delete", - onTrigger: (option) => void remove(option.value.workspace), - }, - ]} - /> - ) -} diff --git a/packages/tui/src/component/dialog-workspace-unavailable.tsx b/packages/tui/src/component/dialog-workspace-unavailable.tsx deleted file mode 100644 index 3181bd8590..0000000000 --- a/packages/tui/src/component/dialog-workspace-unavailable.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { TextAttributes } from "@opentui/core" -import { createStore } from "solid-js/store" -import { For } from "solid-js" -import { useTheme } from "../context/theme" -import { useDialog } from "../ui/dialog" -import { useBindings } from "../keymap" - -export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean | void | Promise }) { - const dialog = useDialog() - const { theme } = useTheme() - const [store, setStore] = createStore({ - active: "restore" as "cancel" | "restore", - }) - - const options = ["cancel", "restore"] as const - - async function confirm() { - if (store.active === "cancel") { - dialog.clear() - return - } - const result = await props.onRestore?.() - if (result === false) return - } - - useBindings(() => ({ - bindings: [ - { key: "return", desc: "Confirm workspace option", group: "Dialog", cmd: () => void confirm() }, - { key: "left", desc: "Cancel workspace restore", group: "Dialog", cmd: () => setStore("active", "cancel") }, - { key: "right", desc: "Restore workspace", group: "Dialog", cmd: () => setStore("active", "restore") }, - ], - })) - - return ( - - - - Workspace Unavailable - - dialog.clear()}> - esc - - - - This session is attached to a workspace that is no longer available. - - - Would you like to restore this session into a new workspace? - - - - {(item) => ( - { - setStore("active", item) - void confirm() - }} - > - {item} - - )} - - - - ) -} diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 1b86dbe582..9b79590d5a 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -22,7 +22,7 @@ import type { PromptInfo, PromptPartRef } from "../../prompt/history" import { useFrecency } from "../../prompt/frecency" import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap" import { displayCharAt, mentionTriggerIndex } from "../../prompt/display" -import type { FileSystemEntry } from "@opencode-ai/sdk/v2" +import type { FileSystemEntry } from "@opencode-ai/client" function removeLineRange(input: string) { const hashIndex = input.lastIndexOf("#") diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 652f8d012f..3ded3161a3 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -37,7 +37,6 @@ import { usePromptStash } from "../../prompt/stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import type { UserMessage } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { errorMessage } from "../../util/error" import { createColors, createFrames } from "../../ui/spinner" @@ -48,11 +47,9 @@ import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { createFadeIn } from "../../util/signal" import { DialogSkill } from "../dialog-skill" -import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "../../context/args" import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap" import { useTuiConfig } from "../../config" -import { usePromptWorkspace } from "./workspace" import { usePromptMove } from "./move" import { readLocalAttachment } from "./local-attachment" import { useData } from "../../context/data" @@ -219,7 +216,6 @@ export function Prompt(props: PromptProps) { }) const editorContextLabelState = createMemo(() => editor.labelState()) const [auto, setAuto] = createSignal() - const workspace = usePromptWorkspace(props.sessionID) const move = usePromptMove({ projectID: () => (props.sessionID ? data.session.get(props.sessionID)?.projectID : undefined) ?? project.project(), sessionID: () => props.sessionID, @@ -269,13 +265,6 @@ export function Prompt(props: PromptProps) { if (!props.disabled) input.cursorColor = theme.text }) - const lastUserMessage = createMemo(() => { - if (!props.sessionID) return undefined - const messages = sync.data.message[props.sessionID] - if (!messages) return undefined - return messages.findLast((m): m is UserMessage => m.role === "user") - }) - const usage = createMemo(() => { if (!props.sessionID) return const session = data.session.get(props.sessionID) @@ -529,17 +518,6 @@ export function Prompt(props: PromptProps) { )) }, }, - { - title: "Warp", - desc: "Change the workspace for the session", - name: "workspace.set", - category: "Session", - enabled: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, - slashName: "warp", - run: () => { - workspace.open() - }, - }, { title: "Move session", desc: "Move to another project dir", @@ -572,7 +550,6 @@ export function Prompt(props: PromptProps) { "prompt.skills", "session.interrupt", "session.background", - "workspace.set", "session.move", ]), })) @@ -957,8 +934,6 @@ export function Prompt(props: PromptProps) { } async function submitInner() { - workspace.clearNotice() - // IME: double-defer may fire before onContentChange flushes the last // composed character (e.g. Korean hangul) to the store, so read // plainText directly and sync before any downstream reads. @@ -967,7 +942,7 @@ export function Prompt(props: PromptProps) { syncExtmarksWithPromptParts() } if (props.disabled) return false - if (workspace.creating() || move.creating()) return false + if (move.creating()) return false if (auto()?.visible) return false if (!store.prompt.text) return false const trimmed = store.prompt.text.trim() @@ -983,29 +958,11 @@ export function Prompt(props: PromptProps) { return false } - const workspaceSession = props.sessionID ? sync.session.get(props.sessionID) : undefined - const workspaceID = workspaceSession?.workspaceID - const workspaceStatus = workspaceID ? (project.workspace.status(workspaceID) ?? "error") : undefined - if (props.sessionID && workspaceID && workspaceStatus !== "connected") { - dialog.replace(() => ( - { - workspace.open() - return false - }} - /> - )) - return false - } - const variant = local.model.variant.current() let sessionID = props.sessionID let session = sessionID ? data.session.get(sessionID) : undefined let finishMoveProgress = false if (sessionID == null) { - const selectedWorkspace = workspace.selection() - const workspaceID = selectedWorkspace?.type === "existing" ? selectedWorkspace.workspaceID : undefined - const directory = await move.getDirectory() if (move.pending() && !directory) return false finishMoveProgress = Boolean(move.progress()) @@ -1013,9 +970,7 @@ export function Prompt(props: PromptProps) { const created = await sdk.api.session .create({ - location: directory - ? { directory, workspaceID } - : { directory: location.directory, workspaceID: workspaceID ?? location.workspaceID }, + location: directory ? { directory } : location, agent: agent.id, model: { providerID: selectedModel.providerID, @@ -1348,7 +1303,7 @@ export function Prompt(props: PromptProps) { const spinnerDef = createMemo(() => { const agent = status() === "running" - ? (local.agent.list().find((agent) => agent.id === lastUserMessage()?.agent) ?? local.agent.current()) + ? local.agent.current() : local.agent.current() const color = agent ? local.agent.color(agent.id) : theme.border return { @@ -1554,41 +1509,6 @@ export function Prompt(props: PromptProps) { - - {(notice) => ( - - {notice()} - - )} - - - {(label) => ( - - - - - - {(() => { - const item = label() - if (item.type === "new") { - if (workspace.creating()) - return `Creating ${item.workspaceType}${".".repeat(workspace.creatingDots())}` - return ( - <> - Workspace (new {item.workspaceType}) - - ) - } - return ( - <> - Workspace {item.workspaceName} - - ) - })()} - - - )} - {(progress) => ( diff --git a/packages/tui/src/component/prompt/workspace.tsx b/packages/tui/src/component/prompt/workspace.tsx deleted file mode 100644 index 57fad0ec3b..0000000000 --- a/packages/tui/src/component/prompt/workspace.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { useDialog } from "../../ui/dialog" -import { useSDK } from "../../context/sdk" -import { useProject } from "../../context/project" -import { useSync } from "../../context/sync" -import { useToast } from "../../ui/toast" -import { errorMessage } from "../../util/error" -import { - confirmWorkspaceFileChanges, - openWorkspaceSelect, - warpWorkspaceSession, - type WorkspaceSelection, -} from "../dialog-workspace-create" -import type { WorkspaceStatus } from "../workspace-label" - -export function usePromptWorkspace(sessionID?: string) { - const dialog = useDialog() - const sdk = useSDK() - const project = useProject() - const sync = useSync() - const toast = useToast() - const [selection, setSelection] = createSignal() - const [creating, setCreating] = createSignal(false) - const [creatingDots, setCreatingDots] = createSignal(3) - const [notice, setNotice] = createSignal() - - async function create(selection: Extract) { - setCreating(true) - let result - try { - result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null }) - } catch (err) { - setSelection(undefined) - setCreating(false) - toast.show({ title: "Creating workspace failed", message: errorMessage(err), variant: "error" }) - return - } - if (result.error || !result.data) { - setSelection(undefined) - setCreating(false) - toast.show({ - title: "Creating workspace failed", - message: errorMessage(result.error ?? "no response"), - variant: "error", - }) - return - } - - await project.workspace.sync() - const workspace = result.data - setSelection({ - type: "existing", - workspaceID: workspace.id, - workspaceType: workspace.type, - workspaceName: workspace.name, - }) - setCreating(false) - return workspace - } - - async function warp(selection: WorkspaceSelection) { - if (!sessionID) { - setSelection(selection) - dialog.clear() - if (selection.type === "new") void create(selection) - return - } - const sourceWorkspaceID = project.workspace.current() - const copyChanges = await confirmWorkspaceFileChanges({ dialog, sdk, sourceWorkspaceID }) - if (copyChanges === undefined) return - setSelection(selection) - dialog.clear() - - const workspace = - selection.type === "none" - ? { id: null, name: "local project" } - : selection.type === "existing" - ? { id: selection.workspaceID, name: selection.workspaceName } - : await create(selection) - if (!workspace) return - - const warped = await warpWorkspaceSession({ - dialog, - sdk, - sync, - project, - toast, - sourceWorkspaceID, - workspaceID: workspace.id, - sessionID, - copyChanges, - }) - if (warped) showNotice(workspace.name) - } - - function showNotice(name: string) { - setNotice(`Warped to ${name}`) - setTimeout(() => setNotice(undefined), 4000) - } - - function clearNotice() { - setNotice(undefined) - } - - function open() { - void openWorkspaceSelect({ dialog, sdk, sync, project, toast, onSelect: warp }) - } - - createEffect(() => { - if (!creating()) { - setCreatingDots(3) - return - } - const timer = setInterval(() => setCreatingDots((dots) => (dots % 3) + 1), 1000) - onCleanup(() => clearInterval(timer)) - }) - - const label = createMemo< - | { type: "new"; workspaceType: string } - | { type: "existing"; workspaceType: string; workspaceName: string; status?: WorkspaceStatus } - | undefined - >(() => { - const selected = selection() - if (!selected) return - if (selected.type === "none") return - if (sessionID && !creating()) return - if (selected.type === "new") return { type: "new", workspaceType: selected.workspaceType } - return { - type: "existing", - workspaceType: selected.workspaceType, - workspaceName: selected.workspaceName, - status: selected.type === "existing" ? "connected" : undefined, - } - }) - - return { selection, creating, creatingDots, notice, label, open, warp, clearNotice } -} diff --git a/packages/tui/src/component/workspace-label.tsx b/packages/tui/src/component/workspace-label.tsx deleted file mode 100644 index 4dd8982ae7..0000000000 --- a/packages/tui/src/component/workspace-label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useTheme } from "../context/theme" - -export type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" - -export function WorkspaceLabel(props: { type: string; name: string; status?: WorkspaceStatus; icon?: boolean }) { - const { theme } = useTheme() - const color = () => { - if (props.status === "connected") return theme.success - if (props.status === "error") return theme.error - return theme.textMuted - } - - return ( - <> - {props.icon ? : undefined} - {props.name} ({props.type}) - - ) -} diff --git a/packages/tui/src/config/keybind.ts b/packages/tui/src/config/keybind.ts index a5bcbc4aa1..7cab6b8c5b 100644 --- a/packages/tui/src/config/keybind.ts +++ b/packages/tui/src/config/keybind.ts @@ -125,7 +125,6 @@ export const Definitions = { model_cycle_favorite_reverse: keybind("none", "Previous favorite model"), mcp_list: keybind("none", "List MCP servers"), provider_connect: keybind("none", "Connect integration"), - console_org_switch: keybind("none", "Switch console organization"), agent_list: keybind("a", "List agents"), agent_cycle: keybind("tab", "Next agent"), agent_cycle_reverse: keybind("shift+tab", "Previous agent"), @@ -156,7 +155,6 @@ export const Definitions = { prompt_stash: keybind("none", "Stash prompt"), prompt_stash_pop: keybind("none", "Pop stashed prompt"), prompt_stash_list: keybind("none", "List stashed prompts"), - workspace_set: keybind("none", "Set workspace"), input_clear: keybind("ctrl+c", "Clear input field"), input_paste: keybind({ key: "ctrl+v", preventDefault: false }, "Paste from clipboard"), @@ -332,7 +330,6 @@ export const CommandMap = { model_cycle_favorite_reverse: "model.cycle_favorite_reverse", mcp_list: "mcp.list", provider_connect: "provider.connect", - console_org_switch: "console.org.switch", agent_list: "agent.list", agent_cycle: "agent.cycle", agent_cycle_reverse: "agent.cycle.reverse", @@ -361,7 +358,6 @@ export const CommandMap = { prompt_stash: "prompt.stash", prompt_stash_pop: "prompt.stash.pop", prompt_stash_list: "prompt.stash.list", - workspace_set: "workspace.set", input_clear: "prompt.clear", input_paste: "prompt.paste", input_submit: "input.submit", diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index e71c3a6985..d714c62edb 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -21,10 +21,10 @@ import type { SessionMessageAssistantText, SessionMessageAssistantTool, SessionInfo, - Shell, + ShellInfo, SkillInfo, -} from "@opencode-ai/sdk/v2" -import type { OpenCodeEvent } from "@opencode-ai/client/promise" + OpenCodeEvent, +} from "@opencode-ai/client" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" @@ -49,7 +49,7 @@ type LocationData = { reference?: ReferenceInfo[] // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. - shell?: Record + shell?: Record skill?: SkillInfo[] } diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index 5c16acdaf8..d61f5f760b 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -453,7 +453,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const slots = createMemo(() => { - const existing = new Set(sync.data.session.filter((x) => x.parentID === undefined).map((x) => x.id)) + const existing = new Set(data.session.list().filter((x) => x.parentID === undefined).map((x) => x.id)) return sessionStore.pinned.filter((id) => existing.has(id)).slice(0, 9) }) diff --git a/packages/tui/src/context/location.tsx b/packages/tui/src/context/location.tsx index 0f3fca1359..52f73c0bbb 100644 --- a/packages/tui/src/context/location.tsx +++ b/packages/tui/src/context/location.tsx @@ -1,4 +1,4 @@ -import type { LocationRef } from "@opencode-ai/sdk/v2" +import type { LocationRef } from "@opencode-ai/client" import { createContext, useContext, type Accessor, type ParentProps } from "solid-js" const context = createContext>() diff --git a/packages/tui/src/context/project.tsx b/packages/tui/src/context/project.tsx index 405cec1894..73584ceda1 100644 --- a/packages/tui/src/context/project.tsx +++ b/packages/tui/src/context/project.tsx @@ -1,11 +1,8 @@ import { batch } from "solid-js" -import type { Path, Workspace } from "@opencode-ai/sdk/v2" import { createStore, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" -type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error" - export const { use: useProject, provider: ProjectProvider } = createSimpleContext({ name: "Project", init: () => { @@ -17,7 +14,7 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex config: "", worktree: "", directory: process.cwd(), - } satisfies Path + } const [store, setStore] = createStore({ project: { @@ -30,42 +27,26 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex }, workspace: { current: undefined as string | undefined, - list: [] as Workspace[], - status: {} as Record, }, }) async function sync() { const workspace = store.workspace.current const location = { workspace } - const [instancePath, project] = await Promise.all([ - sdk.client.path.get({ workspace }), - sdk.api.project.current({ location }), - ]) - const directories = await sdk.api.project.directories({ projectID: project.id, location }) + const current = await sdk.api.location.get({ location }) + const directories = await sdk.api.project.directories({ projectID: current.project.id, location }) batch(() => { - setStore("instance", "path", reconcile(instancePath.data || defaultPath)) - setStore("project", "id", project.id) - setStore("project", "worktree", project.directory) + setStore( + "instance", + "path", + reconcile({ ...defaultPath, worktree: current.project.directory, directory: current.directory }), + ) + setStore("project", "id", current.project.id) + setStore("project", "worktree", current.project.directory) setStore("project", "mainDir", directories.findLast((item) => item.strategy === undefined)?.directory) }) } - async function syncWorkspace() { - const listed = await sdk.client.experimental.workspace.list().catch(() => undefined) - if (!listed?.data) return - const status = await sdk.client.experimental.workspace.status().catch(() => undefined) - const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status])) - - batch(() => { - setStore("workspace", "list", reconcile(listed.data)) - setStore("workspace", "status", reconcile(next)) - if (!listed.data.some((item) => item.id === store.workspace.current)) { - setStore("workspace", "current", undefined) - } - }) - } - return { data: store, project() { @@ -88,19 +69,6 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex if (store.workspace.current === workspace) return setStore("workspace", "current", workspace) }, - list() { - return store.workspace.list - }, - get(workspaceID: string) { - return store.workspace.list.find((item) => item.id === workspaceID) - }, - status(workspaceID: string) { - return store.workspace.status[workspaceID] - }, - statuses() { - return store.workspace.status - }, - sync: syncWorkspace, }, sync, } diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index c35927d6f0..f0133f85c0 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -2,7 +2,6 @@ import type { Agent, Command, Config, - ConsoleState, FormatterStatus, LspStatus, McpResource, @@ -11,8 +10,6 @@ import type { Part, PermissionRequest, Provider, - ProviderAuthMethod, - ProviderListResponse, QuestionRequest, Session, FileDiffInfo, @@ -22,11 +19,6 @@ import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" import { useProject } from "./project" -const emptyConsoleState: ConsoleState = { - consoleManagedProviders: [], - switchableOrgCount: 0, -} - export const { context: SyncContext, use: useSync, @@ -38,10 +30,6 @@ export const { const [store, setStore] = createStore<{ status: "loading" | "partial" | "complete" provider: Provider[] - provider_default: Record - provider_next: ProviderListResponse - console_state: ConsoleState - provider_auth: Record agent: Agent[] command: Command[] permission: Record @@ -59,14 +47,6 @@ export const { }>({ status: "complete", provider: [], - provider_default: {}, - provider_next: { - all: [], - default: {}, - connected: [], - }, - console_state: emptyConsoleState, - provider_auth: {}, agent: [], command: [], permission: {}, diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 1bee45dedc..d5e1e56e7b 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @opentui/solid */ import type { TuiPlugin, TuiPluginApi, TuiRouteCurrent } from "@opencode-ai/plugin/tui" -import type { FileDiffInfo, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, FileDiffLegacyInfo } from "@opencode-ai/client" import { TextAttributes, type BorderSides, @@ -11,6 +11,7 @@ import { import { LANGUAGE_EXTENSIONS } from "../../util/filetype" import { useBindings, useCommandShortcut } from "../../keymap" import { useTheme } from "../../context/theme" +import { useSDK } from "../../context/sdk" import { useTerminalDimensions } from "@opentui/solid" import path from "path" import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js" @@ -43,7 +44,7 @@ const VCS_DIFF_CONTEXT_LINES = 12 const KV_SHOW_FILE_TREE = "diff_viewer_show_file_tree" const KV_SINGLE_PATCH = "diff_viewer_single_patch" const KV_VIEW = "diff_viewer_view" -type DiffMode = "git" | "branch" | "last-turn" +type DiffMode = "working" | "branch" | "last-turn" type DiffViewerFocus = "patches" | "files" type DiffView = "split" | "unified" type SelectedHunk = { readonly fileIndex: number; readonly hunkIndex: number; readonly scrollTop: number } @@ -56,7 +57,7 @@ type DiffFile = { readonly status: "added" | "deleted" | "modified" } -const normalizeDiffs = (diffs: readonly (VcsFileDiff | FileDiffInfo | SnapshotFileDiff)[]): DiffFile[] => +const normalizeDiffs = (diffs: readonly (FileDiffInfo | FileDiffLegacyInfo)[]): DiffFile[] => diffs.flatMap((item) => item.file ? [ @@ -90,6 +91,7 @@ function diffSourceLabel(mode: DiffMode) { function DiffViewer(props: { api: TuiPluginApi }) { const dimensions = useTerminalDimensions() + const sdk = useSDK() const themeState = useTheme() const theme = () => props.api.theme.current const params = () => @@ -101,7 +103,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { returnRoute?: TuiRouteCurrent } | undefined - const mode = () => params()?.mode ?? "git" + const mode = () => params()?.mode ?? "working" const diffInput = createMemo(() => { const sessionID = params()?.sessionID return { @@ -122,9 +124,12 @@ function DiffViewer(props: { api: TuiPluginApi }) { return normalizeDiffs(result.data ?? []) } - const result = await props.api.client.vcs.diff( - { directory: input.directory, mode: input.mode, context: VCS_DIFF_CONTEXT_LINES }, - { throwOnError: true }, + const result = await sdk.api.vcs.diff( + { + location: input.directory ? { directory: input.directory } : undefined, + mode: input.mode, + context: VCS_DIFF_CONTEXT_LINES, + }, ) return normalizeDiffs(result.data ?? []) }) @@ -686,7 +691,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { return [ { title: "Working tree", - value: "git" as const, + value: "working" as const, description: "Show current git changes", }, ...(vcs?.branch && vcs.default_branch && vcs.branch !== vcs.default_branch @@ -1060,7 +1065,7 @@ const tui: TuiPlugin = async (api) => { namespace: "palette", run() { api.route.navigate(ROUTE, { - mode: "git", + mode: "working", sessionID: "params" in api.route.current ? api.route.current.params?.sessionID : undefined, returnRoute: api.route.current, }) diff --git a/packages/tui/src/routes/session/dialog-timeline.tsx b/packages/tui/src/routes/session/dialog-timeline.tsx index b3e162e9ad..c42e32139c 100644 --- a/packages/tui/src/routes/session/dialog-timeline.tsx +++ b/packages/tui/src/routes/session/dialog-timeline.tsx @@ -1,7 +1,6 @@ import { createMemo, onMount } from "solid-js" -import { useSync } from "../../context/sync" +import { useData } from "../../context/data" import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select" -import type { TextPart } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { DialogMessage } from "./dialog-message" import { useDialog } from "../../ui/dialog" @@ -10,7 +9,7 @@ export function DialogTimeline(props: { sessionID: string onMove: (messageID: string) => void }) { - const sync = useSync() + const data = useData() const dialog = useDialog() onMount(() => { @@ -18,16 +17,12 @@ export function DialogTimeline(props: { }) const options = createMemo((): DialogSelectOption[] => { - const messages = sync.data.message[props.sessionID] ?? [] + const messages = data.session.message.list(props.sessionID) const result = [] as DialogSelectOption[] for (const message of messages) { - if (message.role !== "user") continue - const part = (sync.data.part[message.id] ?? []).find( - (x) => x.type === "text" && !x.synthetic && !x.ignored, - ) as TextPart - if (!part) continue + if (message.type !== "user") continue result.push({ - title: part.text.replace(/\n/g, " "), + title: message.text.replace(/\n/g, " "), value: message.id, footer: Locale.time(message.time.created), onSelect: (dialog) => { diff --git a/packages/tui/src/routes/session/footer.tsx b/packages/tui/src/routes/session/footer.tsx index d163f21477..8a419dbddc 100644 --- a/packages/tui/src/routes/session/footer.tsx +++ b/packages/tui/src/routes/session/footer.tsx @@ -17,7 +17,7 @@ export function Footer() { const lsp = createMemo(() => Object.keys(sync.data.lsp)) const permissions = createMemo(() => { if (route.data.type !== "session") return [] - return sync.data.permission[route.data.sessionID] ?? [] + return data.session.permission.list(route.data.sessionID) ?? [] }) const directory = useDirectory() const connected = useConnected() diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index 8c7aec59c7..cde48466a9 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -4,7 +4,7 @@ import { useRenderer, useTerminalDimensions } from "@opentui/solid" import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core" import open from "open" import { selectedForeground, tint, useTheme } from "../../context/theme" -import type { FormField, FormValue } from "@opencode-ai/sdk/v2" +import type { FormField, FormValue } from "@opencode-ai/client" import type { FormWithLocation } from "../../context/data" import { useSDK } from "../../context/sdk" import { useClipboard } from "../../context/clipboard" diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 266e409d3d..b063411457 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -35,7 +35,7 @@ import type { SessionMessageAssistantTool, SessionMessageUser, SessionInfo, -} from "@opencode-ai/sdk/v2" +} from "@opencode-ai/client" import { useLocal } from "../../context/local" import { Locale } from "../../util/locale" import { FilePath } from "../../ui/file-path" diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a0ca4775c3..cfbb2037cc 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -4,7 +4,7 @@ import { createMemo, For, Match, Show, Switch } from "solid-js" import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useTheme, selectedForeground } from "../../context/theme" -import type { PermissionV2Request } from "@opencode-ai/sdk/v2" +import type { PermissionV2Request } from "@opencode-ai/client" import { useSDK } from "../../context/sdk" import { SplitBorder } from "../../ui/border" import { useData } from "../../context/data" diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 59393ff143..1df58d3539 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,4 +1,4 @@ -import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useData } from "../../context/data" diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx index a028200700..d2ebcd2851 100644 --- a/packages/tui/src/routes/session/sidebar.tsx +++ b/packages/tui/src/routes/session/sidebar.tsx @@ -1,4 +1,3 @@ -import { useProject } from "../../context/project" import { useData } from "../../context/data" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" @@ -7,20 +6,13 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version" import { usePluginRuntime } from "../../plugin/runtime" import { getScrollAcceleration } from "../../util/scroll" -import { WorkspaceLabel } from "../../component/workspace-label" export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const pluginRuntime = usePluginRuntime() - const project = useProject() const data = useData() const { theme } = useTheme() const tuiConfig = useTuiConfig() const session = createMemo(() => data.session.get(props.sessionID)) - const workspace = () => { - const workspaceID = session()?.location.workspaceID - if (!workspaceID) return - return project.workspace.get(workspaceID) - } const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) return ( @@ -58,21 +50,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { - - } - > - {(item) => ( - - )} - + {session()!.location.workspaceID} diff --git a/packages/tui/src/util/connected-provider.ts b/packages/tui/src/util/connected-provider.ts index 4ceacf6552..2fbb9f718e 100644 --- a/packages/tui/src/util/connected-provider.ts +++ b/packages/tui/src/util/connected-provider.ts @@ -1,4 +1,4 @@ -import type { IntegrationInfo } from "@opencode-ai/sdk/v2" +import type { IntegrationInfo } from "@opencode-ai/client" export function hasConnectedProvider(integrations: readonly Pick[]) { return integrations.some((integration) => integration.connections.length > 0) diff --git a/packages/tui/src/util/model.ts b/packages/tui/src/util/model.ts index 275112d33d..215602845f 100644 --- a/packages/tui/src/util/model.ts +++ b/packages/tui/src/util/model.ts @@ -1,32 +1,8 @@ -import type { Provider } from "@opencode-ai/sdk/v2" - export function parse(value: string) { const [providerID, ...modelID] = value.split("/") return { providerID, modelID: modelID.join("/") } } -export function index(list: Provider[] | undefined) { - return new Map((list ?? []).map((item) => [item.id, item] as const)) -} - -export function get(list: Provider[] | ReadonlyMap | undefined, providerID: string, modelID: string) { - const provider = - list instanceof Map - ? list.get(providerID) - : Array.isArray(list) - ? list.find((item) => item.id === providerID) - : undefined - return provider?.models[modelID] -} - -export function name( - list: Provider[] | ReadonlyMap | undefined, - providerID: string, - modelID: string, -) { - return get(list, providerID, modelID)?.name ?? modelID -} - export function formatRef(model: { providerID: string; id: string; variant?: string }) { return [model.providerID, model.id, model.variant].filter((value) => value !== undefined).join("/") } diff --git a/packages/tui/src/util/provider-origin.ts b/packages/tui/src/util/provider-origin.ts deleted file mode 100644 index 48d1f852de..0000000000 --- a/packages/tui/src/util/provider-origin.ts +++ /dev/null @@ -1,7 +0,0 @@ -const contains = (consoleManagedProviders: string[] | ReadonlySet, providerID: string) => - Array.isArray(consoleManagedProviders) - ? consoleManagedProviders.includes(providerID) - : consoleManagedProviders.has(providerID) - -export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet, providerID: string) => - contains(consoleManagedProviders, providerID) diff --git a/packages/tui/src/util/session.ts b/packages/tui/src/util/session.ts index 60dddbd1ea..7fe11fb7db 100644 --- a/packages/tui/src/util/session.ts +++ b/packages/tui/src/util/session.ts @@ -1,4 +1,4 @@ -import type { ModelInfo, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" +import type { ModelInfo, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" export function isDefaultTitle(title: string) { return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title) diff --git a/packages/tui/src/util/transcript.ts b/packages/tui/src/util/transcript.ts deleted file mode 100644 index d727c19fb8..0000000000 --- a/packages/tui/src/util/transcript.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" -import { Locale } from "./locale" -import * as Model from "./model" - -export type TranscriptOptions = { - thinking: boolean - toolDetails: boolean - assistantMetadata: boolean - providers?: Provider[] -} - -export type SessionInfo = { - id: string - title: string - time: { - created: number - updated: number - } -} - -export type MessageWithParts = { - info: UserMessage | AssistantMessage - parts: Part[] -} - -export function formatTranscript( - session: SessionInfo, - messages: MessageWithParts[], - options: TranscriptOptions, -): string { - const providers = Model.index(options.providers) - let transcript = `# ${session.title}\n\n` - transcript += `**Session ID:** ${session.id}\n` - transcript += `**Created:** ${new Date(session.time.created).toLocaleString()}\n` - transcript += `**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n` - transcript += `---\n\n` - - for (const msg of messages) { - transcript += formatMessage(msg.info, msg.parts, options, providers) - transcript += `---\n\n` - } - - return transcript -} - -export function formatMessage( - msg: UserMessage | AssistantMessage, - parts: Part[], - options: TranscriptOptions, - providers?: Provider[] | ReadonlyMap, -): string { - let result = "" - - if (msg.role === "user") { - result += `## User\n\n` - } else { - result += formatAssistantHeader(msg, options.assistantMetadata, providers ?? options.providers) - } - - for (const part of parts) { - result += formatPart(part, options) - } - - return result -} - -export function formatAssistantHeader( - msg: AssistantMessage, - includeMetadata: boolean, - providers?: Provider[] | ReadonlyMap, -): string { - if (!includeMetadata) { - return `## Assistant\n\n` - } - - const duration = - msg.time.completed && msg.time.created ? ((msg.time.completed - msg.time.created) / 1000).toFixed(1) + "s" : "" - - const modelName = Model.name(providers, msg.providerID, msg.modelID) - - return `## Assistant (${Locale.titlecase(msg.agent)} · ${modelName}${duration ? ` · ${duration}` : ""})\n\n` -} - -export function formatPart(part: Part, options: TranscriptOptions): string { - if (part.type === "text" && !part.synthetic) { - return `${part.text}\n\n` - } - - if (part.type === "reasoning") { - if (options.thinking) { - return `_Thinking:_\n\n${part.text}\n\n` - } - return "" - } - - if (part.type === "tool") { - let result = `**Tool: ${part.tool}**\n` - if (options.toolDetails && part.state.input) { - result += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`\n` - } - if (options.toolDetails && part.state.status === "completed" && part.state.output) { - result += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`\n` - } - if (options.toolDetails && part.state.status === "error" && part.state.error) { - result += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`\n` - } - result += `\n` - return result - } - - return "" -} diff --git a/packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts b/packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts deleted file mode 100644 index 17e7090cf3..0000000000 --- a/packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { recentConnectedWorkspaces } from "../../../../src/component/dialog-workspace-create" - -describe("recentConnectedWorkspaces", () => { - test("returns connected workspaces sorted by time used", () => { - const workspaces = [ - { id: "wrk_a", name: "alpha", timeUsed: 700 }, - { id: "wrk_b", name: "beta", timeUsed: 800 }, - { id: "wrk_c", name: "gamma", timeUsed: 400 }, - { id: "wrk_d", name: "delta", timeUsed: 300 }, - { id: "wrk_e", name: "epsilon", timeUsed: 200 }, - ] - const status = { - wrk_a: "connected", - wrk_b: "disconnected", - wrk_c: "error", - wrk_d: "connected", - wrk_e: "connected", - } as const - - const { recent } = recentConnectedWorkspaces({ - workspaces, - status: (workspaceID) => status[workspaceID as keyof typeof status], - }) - - expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_a", "wrk_d", "wrk_e"]) - }) -}) diff --git a/packages/tui/test/cli/cmd/tui/integration-options.test.ts b/packages/tui/test/cli/cmd/tui/integration-options.test.ts index f9cc482c6f..a07788248b 100644 --- a/packages/tui/test/cli/cmd/tui/integration-options.test.ts +++ b/packages/tui/test/cli/cmd/tui/integration-options.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { IntegrationInfo } from "@opencode-ai/sdk/v2" +import type { IntegrationInfo } from "@opencode-ai/client" import { connectionSummary, connectMethods, diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index a5931c4481..6f8399fbdd 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import Notifications from "../../../../src/feature-plugins/system/notifications" -import type { OpenCodeEvent } from "@opencode-ai/client/promise" -import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2" +import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client" +import type { Session } from "@opencode-ai/sdk/v2" import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui" import { createTuiPluginApi } from "../../../fixture/tui-plugin" @@ -69,7 +69,7 @@ async function setup() { } } -function question(id: string, sessionID = "session"): QuestionRequest { +function question(id: string, sessionID = "session"): QuestionAsked["data"] { return { id, sessionID, @@ -86,7 +86,7 @@ function form(id: string, sessionID = "session"): Extract { - test("includes a synthetic Other option for custom providers", () => { - expect(providerOptions([{ id: "openai", name: "OpenAI" }]).at(-1)).toMatchObject({ - title: "Other", - description: "Custom provider", - category: "Providers", - }) - }) - - test("does not use Other as the generic provider category", () => { - expect(providerOptions([{ id: "mistral", name: "Mistral" }])[0]?.category).toBe("Providers") - }) - - test("keeps popular providers first and sorts the rest alphabetically", () => { - expect( - providerOptions([ - { id: "openai", name: "OpenAI" }, - { id: "custom-z", name: "Zebra Provider" }, - { id: "anthropic", name: "Anthropic" }, - { id: "mistral", name: "Mistral" }, - { id: "aws", name: "AWS Bedrock" }, - ]).map((option) => option.value), - ).toEqual(["openai", "anthropic", "aws", "mistral", "custom-z", "__opencode_custom_provider__"]) - }) - - test("does not collide with a configured provider named other", () => { - const values = providerOptions([{ id: "other", name: "Other Provider" }]).map((option) => option.value) - expect(new Set(values).size).toBe(values.length) - }) - - test("normalizes and validates custom provider ids", () => { - expect(normalizeCustomProviderID(" custom-provider ")).toBe("custom-provider") - expect(normalizeCustomProviderID("custom_provider")).toBe("custom_provider") - expect(normalizeCustomProviderID("@ai-sdk/custom-provider")).toBe("custom-provider") - expect(normalizeCustomProviderID("-custom-provider")).toBeUndefined() - expect(normalizeCustomProviderID("Custom Provider")).toBeUndefined() - }) -}) diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index f7fc9c0a51..c882e67f14 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -8,21 +8,27 @@ import type { Session } from "@opencode-ai/sdk/v2" import { KVProvider } from "../../../src/context/kv" import { ThemeProvider } from "../../../src/context/theme" import { TuiConfigProvider } from "../../../src/config" +import { SDKProvider } from "../../../src/context/sdk" import { TuiKeybind } from "../../../src/config/keybind" import { OpencodeKeymapProvider } from "../../../src/keymap" import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import { TestTuiContexts } from "../../fixture/tui-environment" +import { createApi, createClient, createEventStream, createFetch, json } from "../../fixture/tui-sdk" test("closing the diff viewer returns to the route it opened from", async () => { const viewer = await renderDiffViewer([]) try { expect(viewer.current()).toEqual({ name: "diff", - params: { mode: "git", sessionID: "session-1", returnRoute: startRoute }, + params: { mode: "working", sessionID: "session-1", returnRoute: startRoute }, + }) + expect(viewer.vcsDiffInput()).toEqual({ + location: { directory: "/repo/session" }, + mode: "working", + context: "12", }) - expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "git", context: 12 }) expect(viewer.commands.has("diff.close")).toBe(true) viewer.commands.get("diff.close")!.run?.({} as never) @@ -108,6 +114,18 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: let vcsDiffInput: unknown let sessionDiffInput: unknown const config = createTuiResolvedConfig() + const transport = createFetch((url) => { + if (url.pathname !== "/api/vcs/diff") return + vcsDiffInput = { + location: { directory: url.searchParams.get("location[directory]") }, + mode: url.searchParams.get("mode"), + context: url.searchParams.get("context"), + } + return json({ + location: { directory: "/repo/session", project: { id: "project-1", directory: "/repo/session" } }, + data: vcsDiff, + }) + }, createEventStream()) function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) @@ -119,12 +137,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: const base = createTuiPluginApi({ keymap, client: { - vcs: { - diff: async (input: unknown) => { - vcsDiffInput = input - return { data: vcsDiff } - }, - }, session: { diff: async (input: unknown) => { sessionDiffInput = input @@ -159,15 +171,17 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: return ( - - - - - {renderDiff?.({ params: "params" in current ? current.params : undefined })} - - - - + + + + + + {renderDiff?.({ params: "params" in current ? current.params : undefined })} + + + + + ) } @@ -218,7 +232,11 @@ test("branch diff source requests branch VCS diff", async () => { name: "diff", params: { mode: "branch", sessionID: "session-1", returnRoute: startRoute }, }) - expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "branch", context: 12 }) + expect(viewer.vcsDiffInput()).toEqual({ + location: { directory: "/repo/session" }, + mode: "branch", + context: "12", + }) expect(viewer.sessionDiffInput()).toBeUndefined() } finally { viewer.app.renderer.destroy() diff --git a/packages/tui/test/cli/tui/session-rows.test.ts b/packages/tui/test/cli/tui/session-rows.test.ts index c43a6faf9d..f6d2701ac7 100644 --- a/packages/tui/test/cli/tui/session-rows.test.ts +++ b/packages/tui/test/cli/tui/session-rows.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import { reduceSessionRows } from "../../../src/routes/session/rows" test("groups exploration parts across assistant messages until a delimiter", () => { diff --git a/packages/tui/test/util/session.test.ts b/packages/tui/test/util/session.test.ts index cd534a40e8..5172aa2694 100644 --- a/packages/tui/test/util/session.test.ts +++ b/packages/tui/test/util/session.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/sdk/v2" +import type { SessionMessageInfo } from "@opencode-ai/client" import { isDefaultTitle, lastAssistantWithUsage } from "../../src/util/session" const assistant = (id: string, input: number): SessionMessageInfo => ({ diff --git a/packages/tui/test/util/transcript.test.ts b/packages/tui/test/util/transcript.test.ts deleted file mode 100644 index 02d6ef0bb9..0000000000 --- a/packages/tui/test/util/transcript.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { formatAssistantHeader, formatMessage, formatPart, formatTranscript } from "../../src/util/transcript" -import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" - -const providers: Provider[] = [ - { - id: "anthropic", - name: "Anthropic", - source: "api", - env: [], - options: {}, - models: { - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - providerID: "anthropic", - api: { - id: "claude-sonnet-4-20250514", - url: "https://example.com/claude-sonnet-4-20250514", - npm: "@ai-sdk/anthropic", - }, - name: "Claude Sonnet 4", - capabilities: { - temperature: true, - reasoning: true, - attachment: true, - toolcall: true, - input: { - text: true, - audio: false, - image: true, - video: false, - pdf: true, - }, - output: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - interleaved: false, - }, - cost: { - input: 0, - output: 0, - cache: { - read: 0, - write: 0, - }, - }, - limit: { - context: 200_000, - output: 8_192, - }, - status: "active", - options: {}, - headers: {}, - release_date: "2025-05-14", - }, - }, - }, -] - -describe("transcript", () => { - describe("formatAssistantHeader", () => { - const baseMsg: AssistantMessage = { - id: "msg_123", - sessionID: "ses_123", - role: "assistant", - agent: "build", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - mode: "", - parentID: "msg_parent", - path: { cwd: "/test", root: "/test" }, - cost: 0.001, - tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1000000, completed: 1005400 }, - } - - test("includes metadata when enabled", () => { - const result = formatAssistantHeader(baseMsg, true) - expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)\n\n") - }) - - test("uses model display name when available", () => { - const result = formatAssistantHeader(baseMsg, true, providers) - expect(result).toBe("## Assistant (Build · Claude Sonnet 4 · 5.4s)\n\n") - }) - - test("excludes metadata when disabled", () => { - const result = formatAssistantHeader(baseMsg, false) - expect(result).toBe("## Assistant\n\n") - }) - - test("handles missing completed time", () => { - const msg = { ...baseMsg, time: { created: 1000000 } } - const result = formatAssistantHeader(msg as AssistantMessage, true) - expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514)\n\n") - }) - - test("titlecases agent name", () => { - const msg = { ...baseMsg, agent: "plan" } - const result = formatAssistantHeader(msg, true) - expect(result).toContain("Plan") - }) - }) - - describe("formatPart", () => { - const options = { thinking: true, toolDetails: true, assistantMetadata: true } - - test("formats text part", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "text", - text: "Hello world", - } - const result = formatPart(part, options) - expect(result).toBe("Hello world\n\n") - }) - - test("skips synthetic text parts", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "text", - text: "Synthetic content", - synthetic: true, - } - const result = formatPart(part, options) - expect(result).toBe("") - }) - - test("formats reasoning when thinking enabled", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "reasoning", - text: "Let me think...", - time: { start: 1000 }, - } - const result = formatPart(part, options) - expect(result).toBe("_Thinking:_\n\nLet me think...\n\n") - }) - - test("skips reasoning when thinking disabled", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "reasoning", - text: "Let me think...", - time: { start: 1000 }, - } - const result = formatPart(part, { ...options, thinking: false }) - expect(result).toBe("") - }) - - test("formats tool part with details", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "tool", - callID: "call_1", - tool: "bash", - state: { - status: "completed", - input: { command: "ls" }, - output: "file1.txt\nfile2.txt", - title: "List files", - metadata: {}, - time: { start: 1000, end: 1100 }, - }, - } - const result = formatPart(part, options) - expect(result).toContain("**Tool: bash**") - expect(result).toContain("**Input:**") - expect(result).toContain('"command": "ls"') - expect(result).toContain("**Output:**") - expect(result).toContain("file1.txt") - }) - - test("formats tool output containing triple backticks without breaking markdown", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "tool", - callID: "call_1", - tool: "bash", - state: { - status: "completed", - input: { command: "echo '```hello```'" }, - output: "```hello```", - title: "Echo backticks", - metadata: {}, - time: { start: 1000, end: 1100 }, - }, - } - const result = formatPart(part, options) - // The tool header should not be inside a code block - expect(result).toStartWith("**Tool: bash**\n") - // Input and output should each be in their own code blocks - expect(result).toContain("**Input:**\n```json") - expect(result).toContain("**Output:**\n```\n```hello```\n```") - }) - - test("formats tool part without details when disabled", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "tool", - callID: "call_1", - tool: "bash", - state: { - status: "completed", - input: { command: "ls" }, - output: "file1.txt", - title: "List files", - metadata: {}, - time: { start: 1000, end: 1100 }, - }, - } - const result = formatPart(part, { ...options, toolDetails: false }) - expect(result).toContain("**Tool: bash**") - expect(result).not.toContain("**Input:**") - expect(result).not.toContain("**Output:**") - }) - - test("formats tool error", () => { - const part: Part = { - id: "part_1", - sessionID: "ses_123", - messageID: "msg_123", - type: "tool", - callID: "call_1", - tool: "bash", - state: { - status: "error", - input: { command: "invalid" }, - error: "Command failed", - time: { start: 1000, end: 1100 }, - }, - } - const result = formatPart(part, options) - expect(result).toContain("**Error:**") - expect(result).toContain("Command failed") - }) - }) - - describe("formatMessage", () => { - const options = { thinking: true, toolDetails: true, assistantMetadata: true, providers } - - test("formats user message", () => { - const msg: UserMessage = { - id: "msg_123", - sessionID: "ses_123", - role: "user", - agent: "build", - model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }, - time: { created: 1000000 }, - } - const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }] - const result = formatMessage(msg, parts, options) - expect(result).toContain("## User") - expect(result).toContain("Hello") - }) - - test("formats assistant message with metadata", () => { - const msg: AssistantMessage = { - id: "msg_123", - sessionID: "ses_123", - role: "assistant", - agent: "build", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - mode: "", - parentID: "msg_parent", - path: { cwd: "/test", root: "/test" }, - cost: 0.001, - tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1000000, completed: 1005400 }, - } - const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }] - const result = formatMessage(msg, parts, options) - expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 5.4s)") - expect(result).toContain("Hi there") - }) - }) - - describe("formatTranscript", () => { - test("formats complete transcript", () => { - const session = { - id: "ses_abc123", - title: "Test Session", - time: { created: 1000000000000, updated: 1000000001000 }, - } - const messages = [ - { - info: { - id: "msg_1", - sessionID: "ses_abc123", - role: "user" as const, - agent: "build", - model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }, - time: { created: 1000000000000 }, - }, - parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }], - }, - { - info: { - id: "msg_2", - sessionID: "ses_abc123", - role: "assistant" as const, - agent: "build", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - mode: "", - parentID: "msg_1", - path: { cwd: "/test", root: "/test" }, - cost: 0.001, - tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1000000000100, completed: 1000000000600 }, - }, - parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }], - }, - ] - const options = { - thinking: false, - toolDetails: false, - assistantMetadata: true, - providers, - } - - const result = formatTranscript(session, messages, options) - - expect(result).toContain("# Test Session") - expect(result).toContain("**Session ID:** ses_abc123") - expect(result).toContain("## User") - expect(result).toContain("Hello") - expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 0.5s)") - expect(result).toContain("Hi!") - expect(result).toContain("---") - }) - - test("falls back to raw model id when provider data is missing", () => { - const session = { - id: "ses_abc123", - title: "Test Session", - time: { created: 1000000000000, updated: 1000000001000 }, - } - const messages = [ - { - info: { - id: "msg_1", - sessionID: "ses_abc123", - role: "assistant" as const, - agent: "build", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - mode: "", - parentID: "msg_0", - path: { cwd: "/test", root: "/test" }, - cost: 0.001, - tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1000000000100, completed: 1000000000600 }, - }, - parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }], - }, - ] - - const result = formatTranscript(session, messages, { - thinking: false, - toolDetails: false, - assistantMetadata: true, - }) - - expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)") - }) - - test("formats transcript without assistant metadata", () => { - const session = { - id: "ses_abc123", - title: "Test Session", - time: { created: 1000000000000, updated: 1000000001000 }, - } - const messages = [ - { - info: { - id: "msg_1", - sessionID: "ses_abc123", - role: "assistant" as const, - agent: "build", - modelID: "claude-sonnet-4-20250514", - providerID: "anthropic", - mode: "", - parentID: "msg_0", - path: { cwd: "/test", root: "/test" }, - cost: 0.001, - tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1000000000100, completed: 1000000000600 }, - }, - parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }], - }, - ] - const options = { thinking: false, toolDetails: false, assistantMetadata: false } - - const result = formatTranscript(session, messages, options) - - expect(result).toContain("## Assistant\n\n") - expect(result).not.toContain("Build") - expect(result).not.toContain("claude-sonnet-4-20250514") - }) - }) -})