refactor(tui): remove legacy sdk surfaces
This commit is contained in:
parent
56a7c06a80
commit
2a7e32c416
54 changed files with 278 additions and 2324 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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("#")
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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("/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue