refactor(tui): remove legacy sdk surfaces

This commit is contained in:
Dax Raad 2026-07-11 16:29:06 -04:00
commit 2a7e32c416
54 changed files with 278 additions and 2324 deletions

View file

@ -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<string>
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<string>
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<string>
metadata: { [x: string]: unknown }
metadata: { [x: string]: JsonValue }
always: Array<string>
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<string>
save?: Array<string>
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<QuestionV2Answer> }
@ -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<QuestionAnswer> }
@ -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<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
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<LLMToolContent>
}
}
@ -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<LLMToolContent>
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<QuestionV2Info>; 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<QuestionInfo>; 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<IntegrationMethod>
connections: Array<ConnectionInfo>
}
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<IntegrationMethod>; connections: Array<ConnectionInfo> }>
data: Array<IntegrationInfo>
}
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<IntegrationMethod>; connections: Array<ConnectionInfo> } | 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<Shell1>
data: Array<ShellInfo1>
}
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<ReferenceInfo>
}
export type ProjectCopyCreateInput = {

View file

@ -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,

View file

@ -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 }),

View file

@ -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)

View file

@ -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: [

View file

@ -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,

View file

@ -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",

View file

@ -824,7 +824,10 @@ function structuralTypes(schemas: ReadonlyArray<Schema.Top>, mutable: boolean, r
const pattern = `(?<![A-Za-z0-9_$.'"])${reference.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$.'"])`
type = type.replace(new RegExp(pattern, "g"), name)
}
const output = type.replaceAll(/ & Brand\.Brand<"[^"]+">/g, "").replaceAll("Schema.Json", "JsonValue")
const output = type
.replaceAll(/ & Brand\.Brand<"[^"]+">/g, "")
.replaceAll("Schema.Json", "JsonValue")
.replaceAll(/(?<!["'])\bunknown\b(?!["'])/g, "JsonValue")
return mutable ? mutableType(output) : output
}
return {

View file

@ -92,12 +92,13 @@ export const Ref = Schema.Struct({
name: Schema.String,
}).annotate({ identifier: "Integration.Ref" })
export class Info extends Schema.Class<Info>("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<typeof Info> {}
export const AttemptID = Schema.String.pipe(
Schema.brand("Integration.AttemptID"),

View file

@ -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<Info>("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<typeof Info> {}

View file

@ -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<typeof Time> {}
@ -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<typeof Info> {}
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) }

View file

@ -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<string[]>; 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<string[]>; 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(() => <DialogWorkspaceList />)
},
},
...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<string[]>; 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(() => <DialogConsoleOrg />)
},
category: "Provider",
},
]
: []),
{
name: "opencode.status",
title: "View status",
@ -1040,7 +989,6 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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()
},
},

View file

@ -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<OrgOption, "accountEmail" | "accountUrl">) =>
`${item.accountEmail} ${accountHost(item.accountUrl)}`
export function DialogConsoleOrg() {
const sdk = useSDK()
const dialog = useDialog()
const toast = useToast()
const { theme } = useTheme()
const [loadError, setLoadError] = createSignal<unknown>()
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: (
<box flexDirection="row" gap={2}>
<text fg={theme.accent}>{item.accountEmail}</text>
<text fg={theme.textMuted}>{accountHost(item.accountUrl)}</text>
</box>
),
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 (
<DialogSelect<string | OrgOption>
title="Switch org"
options={options()}
current={current()}
renderFilter={!showError()}
locked={showError()}
emptyView={
showError() ? (
<box paddingLeft={4} paddingRight={4}>
<text fg={theme.error} attributes={TextAttributes.BOLD}>
Could not load orgs
</text>
<text fg={theme.textMuted}>{errorMessage(loadError())}</text>
</box>
) : undefined
}
/>
)
}

View file

@ -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"

View file

@ -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"

View file

@ -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) => ({

View file

@ -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<string, number> = {
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<string | undefined> {
const value = await DialogPrompt.show(dialog, "Other", {
placeholder: "Provider id",
description: () => (
<text fg={theme.textMuted}>
This only stores a credential. Configure the provider in opencode.json to use it.
</text>
),
})
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(() => <ApiMethod providerID={providerID} title="API key" custom />)
},
}
}
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() ? () => <text fg={theme.success}></text> : 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<number | null>((resolve) => {
dialog.replace(
() => (
<DialogSelect
title="Select auth method"
options={methods.map((x, index) => ({
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<string, string> | 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(() => (
<CodeMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
))
}
if (result.data?.method === "auto") {
dialog.replace(() => (
<AutoMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
))
}
}
if (method.type === "api") {
let metadata: Record<string, string> | undefined
if (method.prompts?.length) {
const value = await PromptsMethod({ dialog, prompts: method.prompts })
if (!value) return
metadata = value
}
return dialog.replace(() => (
<ApiMethod providerID={providerID} title={method.label} metadata={metadata} />
))
}
},
}
}),
)
})
return options
}
export function DialogProvider() {
const options = createDialogProviderOptions()
return <DialogSelect title="Connect a provider" options={options()} />
}
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(() => <DialogModel providerID={props.providerID} />)
})
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={1}>
<Link href={props.authorization.url} fg={theme.primary} />
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
</box>
<text fg={theme.textMuted}>Waiting for authorization...</text>
<text fg={theme.text}>
c <span style={{ fg: theme.textMuted }}>copy</span>
</text>
</box>
)
}
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 (
<DialogPrompt
title={props.title}
placeholder="Authorization code"
onConfirm={async (value) => {
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(() => <DialogModel providerID={props.providerID} />)
return
}
setError(true)
}}
description={() => (
<box gap={1}>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
<Link href={props.authorization.url} fg={theme.primary} />
<Show when={error()}>
<text fg={theme.error}>Invalid code</text>
</Show>
</box>
)}
/>
)
}
interface ApiMethodProps {
providerID: string
title: string
metadata?: Record<string, string>
custom?: boolean
}
function ApiMethod(props: ApiMethodProps) {
const dialog = useDialog()
const sdk = useSDK()
const sync = useSync()
const toast = useToast()
const { theme } = useTheme()
return (
<DialogPrompt
title={props.title}
placeholder="API key"
description={() =>
({
opencode: (
<box gap={1}>
<text fg={theme.textMuted}>
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API
key.
</text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
</text>
</box>
),
"opencode-go": (
<box gap={1}>
<text fg={theme.textMuted}>
OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models
with generous usage limits.
</text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/go</span> and enable OpenCode Go
</text>
</box>
),
})[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(() => <DialogModel providerID={props.providerID} />)
}}
/>
)
}
interface PromptsMethodProps {
dialog: ReturnType<typeof useDialog>
prompts: NonNullable<ProviderAuthMethod["prompts"]>[number][]
}
async function PromptsMethod(props: PromptsMethodProps) {
const inputs: Record<string, string> = {}
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<string | null>((resolve) => {
props.dialog.replace(
() => (
<DialogSelect
title={prompt.message}
options={prompt.options.map((x) => ({
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<string | null>((resolve) => {
props.dialog.replace(
() => (
<DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} />
),
() => resolve(null),
)
})
if (value === null) return null
inputs[prompt.key] = value
}
return inputs
}

View file

@ -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"

View file

@ -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

View file

@ -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<WorkspaceInfo extends { id: string; timeUsed: number | string }>(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 `<system-reminder>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.</system-reminder>`
}
async function loadWorkspaceAdapters(input: {
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
}) {
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<typeof useDialog>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
project: ReturnType<typeof useProject>
toast: ReturnType<typeof useToast>
onSelect: (selection: WorkspaceSelection) => Promise<void> | 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(() => <DialogWorkspaceSelect adapters={adapters} onSelect={input.onSelect} />)
}
export async function warpWorkspaceSession(input: {
dialog: ReturnType<typeof useDialog>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
project: ReturnType<typeof useProject>
toast: ReturnType<typeof useToast>
sourceWorkspaceID?: string
workspaceID: string | null
sessionID: string
copyChanges: boolean
done?: () => void
}): Promise<boolean> {
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<typeof useDialog>
sdk: ReturnType<typeof useSDK>
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> | void
}) {
const dialog = useDialog()
const project = useProject()
const route = useRoute()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const [adapters, setAdapters] = createSignal<Adapter[] | undefined>(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<DialogSelectOption<WorkspaceSelectValue>[]>(() => {
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 (
<DialogSelect<WorkspaceSelectValue>
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(() => (
<DialogExistingWorkspaceSelect omitWorkspaceID={omittedWorkspaceID()} onSelect={props.onSelect} />
))
}}
/>
)
}
function DialogExistingWorkspaceSelect(props: {
omitWorkspaceID?: string
onSelect: (selection: WorkspaceSelection) => Promise<void> | void
}) {
const project = useProject()
const options = createMemo<DialogSelectOption<ExistingWorkspaceSelectValue>[]>(() =>
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 (
<DialogSelect<ExistingWorkspaceSelectValue>
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,
})
}}
/>
)
}

View file

@ -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"

View file

@ -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<string>()
const [removing, setRemoving] = createSignal<string>()
const [expanded, setExpanded] = createStore<Record<string, boolean>>({})
const current = createMemo(() => {
if (route.data.type === "session") return sync.session.get(route.data.sessionID)?.workspaceID
return project.workspace.current()
})
const options = createMemo<DialogSelectOption<WorkspaceOption>[]>(() =>
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: () => <text fg={status === "connected" ? theme.success : theme.error}></text>,
}
}),
)
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 (
<DialogSelect
title="Workspaces"
options={options()}
onMove={(option) => {
setDeleting(undefined)
}}
onSelect={(option) => showDetails(option.value.workspace)}
actions={[
{
command: "session.delete",
title: "delete",
onTrigger: (option) => void remove(option.value.workspace),
},
]}
/>
)
}

View file

@ -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<boolean | void> }) {
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 (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Workspace Unavailable
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<text fg={theme.textMuted} wrapMode="word">
This session is attached to a workspace that is no longer available.
</text>
<text fg={theme.textMuted} wrapMode="word">
Would you like to restore this session into a new workspace?
</text>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
<For each={options}>
{(item) => (
<box
paddingLeft={2}
paddingRight={2}
backgroundColor={item === store.active ? theme.primary : undefined}
onMouseUp={() => {
setStore("active", item)
void confirm()
}}
>
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
</box>
)}
</For>
</box>
</box>
)
}

View file

@ -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("#")

View file

@ -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<AutocompleteRef>()
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(() => (
<DialogWorkspaceUnavailable
onRestore={() => {
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) {
</text>
</box>
</Match>
<Match when={workspace.notice()}>
{(notice) => (
<box paddingLeft={3}>
<text fg={theme.accent}>{notice()}</text>
</box>
)}
</Match>
<Match when={workspace.label()}>
{(label) => (
<box paddingLeft={3} flexDirection="row" gap={1}>
<Show when={workspace.creating()}>
<Spinner color={theme.accent} />
</Show>
<text fg={workspace.creating() ? theme.accent : theme.text}>
{(() => {
const item = label()
if (item.type === "new") {
if (workspace.creating())
return `Creating ${item.workspaceType}${".".repeat(workspace.creatingDots())}`
return (
<>
Workspace <span style={{ fg: theme.textMuted }}>(new {item.workspaceType})</span>
</>
)
}
return (
<>
Workspace <span style={{ fg: theme.textMuted }}>{item.workspaceName}</span>
</>
)
})()}
</text>
</box>
)}
</Match>
<Match when={move.progress()}>
{(progress) => (
<box paddingLeft={3}>

View file

@ -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<WorkspaceSelection>()
const [creating, setCreating] = createSignal(false)
const [creatingDots, setCreatingDots] = createSignal(3)
const [notice, setNotice] = createSignal<string>()
async function create(selection: Extract<WorkspaceSelection, { type: "new" }>) {
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 }
}

View file

@ -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 ? <span style={{ fg: color() }}> </span> : undefined}
<span style={{ fg: theme.text }}>{props.name}</span> <span style={{ fg: theme.textMuted }}>({props.type})</span>
</>
)
}

View file

@ -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("<leader>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",

View file

@ -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<string, Shell>
shell?: Record<string, ShellInfo>
skill?: SkillInfo[]
}

View file

@ -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)
})

View file

@ -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<Accessor<LocationRef | undefined>>()

View file

@ -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<string, WorkspaceStatus>,
},
})
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,
}

View file

@ -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<string, string>
provider_next: ProviderListResponse
console_state: ConsoleState
provider_auth: Record<string, ProviderAuthMethod[]>
agent: Agent[]
command: Command[]
permission: Record<string, PermissionRequest[]>
@ -59,14 +47,6 @@ export const {
}>({
status: "complete",
provider: [],
provider_default: {},
provider_next: {
all: [],
default: {},
connected: [],
},
console_state: emptyConsoleState,
provider_auth: {},
agent: [],
command: [],
permission: {},

View file

@ -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,
})

View file

@ -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<string>[] => {
const messages = sync.data.message[props.sessionID] ?? []
const messages = data.session.message.list(props.sessionID)
const result = [] as DialogSelectOption<string>[]
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) => {

View file

@ -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()

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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 }) {
</text>
<Show when={session()!.location.workspaceID}>
<text fg={theme.textMuted}>
<Show
when={workspace()}
fallback={
<WorkspaceLabel type="unknown" name={session()!.location.workspaceID!} status="error" icon />
}
>
{(item) => (
<WorkspaceLabel
type={item().type}
name={item().name}
status={project.workspace.status(item().id) ?? "error"}
icon
/>
)}
</Show>
{session()!.location.workspaceID}
</text>
</Show>
</box>

View file

@ -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<IntegrationInfo, "connections">[]) {
return integrations.some((integration) => integration.connections.length > 0)

View file

@ -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<string, Provider> | 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<string, Provider> | 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("/")
}

View file

@ -1,7 +0,0 @@
const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
Array.isArray(consoleManagedProviders)
? consoleManagedProviders.includes(providerID)
: consoleManagedProviders.has(providerID)
export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
contains(consoleManagedProviders, providerID)

View file

@ -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)

View file

@ -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, Provider>,
): 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, Provider>,
): 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 ""
}

View file

@ -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"])
})
})

View file

@ -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,

View file

@ -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<OpenCodeEvent, { type:
}
}
function permission(id: string, sessionID = "session"): PermissionRequest {
function permission(id: string, sessionID = "session"): PermissionAsked["data"] {
return {
id,
sessionID,

View file

@ -1,41 +0,0 @@
import { describe, expect, test } from "bun:test"
import { normalizeCustomProviderID, providerOptions } from "../../../../src/component/dialog-provider"
describe("providerOptions", () => {
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()
})
})

View file

@ -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 (
<TestTuiContexts>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</SDKProvider>
</TestTuiContexts>
)
}
@ -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()

View file

@ -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", () => {

View file

@ -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 => ({

View file

@ -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")
})
})
})