Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
const render = ToolRegistry.render(props.part.tool) ?? GenericTool

const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
const input = props.part.state.input ?? {}
const input = props.part.state.displayInput ?? props.part.state.input ?? {}
const container = ToolRegistry.container(props.part.tool)
const permissions = sync.data.permission[props.message.sessionID] ?? []
const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export namespace MessageV2 {
.object({
status: z.literal("pending"),
input: z.record(z.string(), z.any()),
displayInput: z.any().optional(),
raw: z.string(),
})
.meta({
Expand All @@ -219,6 +220,7 @@ export namespace MessageV2 {
.object({
status: z.literal("running"),
input: z.record(z.string(), z.any()),
displayInput: z.any().optional(),
title: z.string().optional(),
metadata: z.record(z.string(), z.any()).optional(),
time: z.object({
Expand All @@ -234,6 +236,7 @@ export namespace MessageV2 {
.object({
status: z.literal("completed"),
input: z.record(z.string(), z.any()),
displayInput: z.any().optional(),
output: z.string(),
title: z.string(),
metadata: z.record(z.string(), z.any()),
Expand All @@ -253,6 +256,7 @@ export namespace MessageV2 {
.object({
status: z.literal("error"),
input: z.record(z.string(), z.any()),
displayInput: z.any().optional(),
error: z.string(),
metadata: z.record(z.string(), z.any()).optional(),
time: z.object({
Expand Down Expand Up @@ -511,7 +515,7 @@ export namespace MessageV2 {
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
toolCallId: part.callID,
input: part.state.input,
input: part.state.displayInput ?? part.state.input,
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
callProviderMetadata: part.metadata,
})
Expand All @@ -521,7 +525,7 @@ export namespace MessageV2 {
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-error",
toolCallId: part.callID,
input: part.state.input,
input: part.state.displayInput ?? part.state.input,
errorText: part.state.error,
callProviderMetadata: part.metadata,
})
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,17 @@ export namespace SessionProcessor {
case "tool-call": {
const match = toolcalls[value.toolCallId]
if (match) {
const { displayInput, ...execArgs } = value.input as Record<string, unknown> & {
displayInput?: unknown
}
const displayArgs = displayInput ?? value.input
const part = await Session.updatePart({
...match,
tool: value.toolName,
state: {
status: "running",
input: value.input,
input: execArgs,
displayInput: displayArgs,
time: {
start: Date.now(),
},
Expand Down Expand Up @@ -186,7 +191,8 @@ export namespace SessionProcessor {
...match,
state: {
status: "completed",
input: value.input,
input: match.state.input !== undefined ? match.state.input : value.input,
displayInput: match.state.displayInput,
output: value.output.output,
metadata: value.output.metadata,
title: value.output.title,
Expand All @@ -210,7 +216,8 @@ export namespace SessionProcessor {
...match,
state: {
status: "error",
input: value.input,
input: match.state.input !== undefined ? match.state.input : value.input,
displayInput: match.state.displayInput,
error: (value.error as any).toString(),
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
time: {
Expand Down
52 changes: 39 additions & 13 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,12 @@ export namespace SessionPrompt {
for (const item of await ToolRegistry.tools(input.model.providerID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
if (schema.type === "object" && schema.properties) {
schema.properties.displayInput = {
type: "object",
description: "the inputs to display in the TUI",
}
}
tools[item.id] = tool({
id: item.id as any,
description: item.description,
Expand All @@ -602,7 +608,9 @@ export namespace SessionPrompt {
args,
},
)
const result = await item.execute(args, {
const { displayInput, ...execArgs } = args
const displayArgs = displayInput ?? args
const result = await item.execute(execArgs, {
sessionID: input.sessionID,
abort: options.abortSignal!,
messageID: input.processor.message.id,
Expand All @@ -612,18 +620,21 @@ export namespace SessionPrompt {
metadata: async (val) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await Session.updatePart({
...match,
state: {
title: val.title,
metadata: val.metadata,
status: "running",
input: args,
time: {
start: Date.now(),
},
// Keep in-memory toolcall state in sync so processor can preserve display input
// This is so plugins can transform how bash is displayed. See #5321
match.state = {
...match.state,
title: val.title,
metadata: val.metadata,
status: "running",
input: execArgs,
displayInput: displayArgs,
time: match.state.time ?? {
start: Date.now(),
},
})
}

await Session.updatePart(match)
}
},
})
Expand Down Expand Up @@ -652,6 +663,20 @@ export namespace SessionPrompt {
if (!execute) continue

// Wrap execute to add plugin hooks and format output
const schema = (
item.inputSchema as {
jsonSchema?: {
type?: string
properties?: Record<string, unknown>
}
}
).jsonSchema
if (schema && schema.type === "object" && schema.properties) {
schema.properties.displayInput = {
type: "object",
description: "the inputs to display in the TUI",
}
}
item.execute = async (args, opts) => {
await Plugin.trigger(
"tool.execute.before",
Expand All @@ -664,7 +689,8 @@ export namespace SessionPrompt {
args,
},
)
const result = await execute(args, opts)
const { displayInput: _, ...execArgs } = args
const result = await execute(execArgs, opts)

await Plugin.trigger(
"tool.execute.after",
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export type ToolStatePending = {
input: {
[key: string]: unknown
}
displayInput?: unknown
raw: string
}

Expand All @@ -273,6 +274,7 @@ export type ToolStateRunning = {
input: {
[key: string]: unknown
}
displayInput?: unknown
title?: string
metadata?: {
[key: string]: unknown
Expand All @@ -287,6 +289,7 @@ export type ToolStateCompleted = {
input: {
[key: string]: unknown
}
displayInput?: unknown
output: string
title: string
metadata: {
Expand All @@ -305,6 +308,7 @@ export type ToolStateError = {
input: {
[key: string]: unknown
}
displayInput?: unknown
error: string
metadata?: {
[key: string]: unknown
Expand Down
Loading