summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/provider/provider.ts12
-rw-r--r--packages/opencode/src/session/index.ts473
-rw-r--r--packages/opencode/src/session/message-v2.ts10
-rw-r--r--packages/opencode/src/session/message.ts14
-rw-r--r--packages/opencode/src/storage/storage.ts2
-rw-r--r--packages/opencode/src/tool/task.ts21
-rw-r--r--packages/tui/internal/components/chat/message.go229
7 files changed, 376 insertions, 385 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 45c06206b..8e7243ba5 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -21,7 +21,7 @@ import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
-// import { TaskTool } from "../tool/task"
+import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -456,7 +456,7 @@ export namespace Provider {
WriteTool,
TodoWriteTool,
TodoReadTool,
- // TaskTool,
+ TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
@@ -531,12 +531,4 @@ export namespace Provider {
providerID: z.string(),
}),
)
-
- export const AuthError = NamedError.create(
- "ProviderAuthError",
- z.object({
- providerID: z.string(),
- message: z.string(),
- }),
- )
}
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 3a92cbeae..71514a295 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -443,7 +443,7 @@ export namespace Session {
const result = await ReadTool.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
- messageID: "", // read tool doesn't use message ID
+ messageID: userMsg.id,
metadata: async () => {},
})
return [
@@ -577,20 +577,22 @@ export namespace Session {
await updateMessage(assistantMsg)
const tools: Record<string, AITool> = {}
+ const processor = createProcessor(assistantMsg, model.info)
+
for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue
+ if (session.parentID && item.id === "task") continue
tools[item.id] = tool({
id: item.id as any,
description: item.description,
inputSchema: item.parameters as ZodSchema,
- async execute(args) {
+ async execute(args, options) {
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
messageID: assistantMsg.id,
- metadata: async () => {
- /*
- const match = toolCalls[opts.toolCallId]
+ metadata: async (val) => {
+ const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await updatePart({
...match,
@@ -598,14 +600,13 @@ export namespace Session {
title: val.title,
metadata: val.metadata,
status: "running",
- input: args.input,
+ input: args,
time: {
start: Date.now(),
},
},
})
}
- */
},
})
return result
@@ -676,257 +677,260 @@ export namespace Session {
],
}),
})
- const result = await processStream(assistantMsg, model.info, stream)
+ const result = await processor.process(stream)
return result
}
- async function processStream(
- assistantMsg: MessageV2.Assistant,
- model: ModelsDev.Model,
- stream: StreamTextResult<Record<string, AITool>, never>,
- ) {
- try {
- let currentText: MessageV2.TextPart | undefined
- const toolCalls: Record<string, MessageV2.ToolPart> = {}
+ function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
+ const toolCalls: Record<string, MessageV2.ToolPart> = {}
+ return {
+ partFromToolCall(toolCallID: string) {
+ return toolCalls[toolCallID]
+ },
+ async process(stream: StreamTextResult<Record<string, AITool>, never>) {
+ try {
+ let currentText: MessageV2.TextPart | undefined
- for await (const value of stream.fullStream) {
- log.info("part", {
- type: value.type,
- })
- switch (value.type) {
- case "start":
- const snapshot = await Snapshot.create(assistantMsg.sessionID)
- if (snapshot)
- await updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID: assistantMsg.sessionID,
- type: "snapshot",
- snapshot,
- })
- break
-
- case "tool-input-start":
- const part = await updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID: assistantMsg.sessionID,
- type: "tool",
- tool: value.toolName,
- callID: value.id,
- state: {
- status: "pending",
- },
+ for await (const value of stream.fullStream) {
+ log.info("part", {
+ type: value.type,
})
- toolCalls[value.id] = part as MessageV2.ToolPart
- break
-
- case "tool-input-delta":
- break
-
- case "tool-call": {
- const match = toolCalls[value.toolCallId]
- if (match) {
- const part = await updatePart({
- ...match,
- state: {
- status: "running",
- input: value.input,
- time: {
- start: Date.now(),
- },
- },
- })
- toolCalls[value.toolCallId] = part as MessageV2.ToolPart
- }
- break
- }
- case "tool-result": {
- const match = toolCalls[value.toolCallId]
- if (match && match.state.status === "running") {
- await updatePart({
- ...match,
- state: {
- status: "completed",
- input: value.input,
- output: value.output.output,
- metadata: value.output.metadata,
- title: value.output.title,
- time: {
- start: match.state.time.start,
- end: Date.now(),
+ switch (value.type) {
+ case "start":
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
+ break
+
+ case "tool-input-start":
+ const part = await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "tool",
+ tool: value.toolName,
+ callID: value.id,
+ state: {
+ status: "pending",
},
- },
- })
- delete toolCalls[value.toolCallId]
- const snapshot = await Snapshot.create(assistantMsg.sessionID)
- if (snapshot)
+ })
+ toolCalls[value.id] = part as MessageV2.ToolPart
+ break
+
+ case "tool-input-delta":
+ break
+
+ case "tool-call": {
+ const match = toolCalls[value.toolCallId]
+ if (match) {
+ const part = await updatePart({
+ ...match,
+ state: {
+ status: "running",
+ input: value.input,
+ time: {
+ start: Date.now(),
+ },
+ },
+ })
+ toolCalls[value.toolCallId] = part as MessageV2.ToolPart
+ }
+ break
+ }
+ case "tool-result": {
+ const match = toolCalls[value.toolCallId]
+ if (match && match.state.status === "running") {
+ await updatePart({
+ ...match,
+ state: {
+ status: "completed",
+ input: value.input,
+ output: value.output.output,
+ metadata: value.output.metadata,
+ title: value.output.title,
+ time: {
+ start: match.state.time.start,
+ end: Date.now(),
+ },
+ },
+ })
+ delete toolCalls[value.toolCallId]
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
+ }
+ break
+ }
+
+ case "tool-error": {
+ const match = toolCalls[value.toolCallId]
+ if (match && match.state.status === "running") {
+ await updatePart({
+ ...match,
+ state: {
+ status: "error",
+ input: value.input,
+ error: (value.error as any).toString(),
+ time: {
+ start: match.state.time.start,
+ end: Date.now(),
+ },
+ },
+ })
+ delete toolCalls[value.toolCallId]
+ const snapshot = await Snapshot.create(assistantMsg.sessionID)
+ if (snapshot)
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "snapshot",
+ snapshot,
+ })
+ }
+ break
+ }
+
+ case "error":
+ throw value.error
+
+ case "start-step":
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
- type: "snapshot",
- snapshot,
+ type: "step-start",
})
- }
- break
- }
+ break
- case "tool-error": {
- const match = toolCalls[value.toolCallId]
- if (match && match.state.status === "running") {
- await updatePart({
- ...match,
- state: {
- status: "error",
- input: value.input,
- error: (value.error as any).toString(),
- time: {
- start: match.state.time.start,
- end: Date.now(),
- },
- },
- })
- delete toolCalls[value.toolCallId]
- const snapshot = await Snapshot.create(assistantMsg.sessionID)
- if (snapshot)
+ case "finish-step":
+ const usage = getUsage(model, value.usage, value.providerMetadata)
+ assistantMsg.cost += usage.cost
+ assistantMsg.tokens = usage.tokens
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
- type: "snapshot",
- snapshot,
+ type: "step-finish",
+ tokens: usage.tokens,
+ cost: usage.cost,
})
- }
- break
- }
+ await updateMessage(assistantMsg)
+ break
- case "error":
- throw value.error
-
- case "start-step":
- await updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID: assistantMsg.sessionID,
- type: "step-start",
- })
- break
-
- case "finish-step":
- const usage = getUsage(model, value.usage, value.providerMetadata)
- assistantMsg.cost += usage.cost
- assistantMsg.tokens = usage.tokens
- await updatePart({
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID: assistantMsg.sessionID,
- type: "step-finish",
- tokens: usage.tokens,
- cost: usage.cost,
- })
- await updateMessage(assistantMsg)
- break
-
- case "text-start":
- currentText = {
- id: Identifier.ascending("part"),
- messageID: assistantMsg.id,
- sessionID: assistantMsg.sessionID,
- type: "text",
- text: "",
- time: {
- start: Date.now(),
- },
- }
- break
+ case "text-start":
+ currentText = {
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
+ type: "text",
+ text: "",
+ time: {
+ start: Date.now(),
+ },
+ }
+ break
- case "text":
- if (currentText) {
- currentText.text += value.text
- await updatePart(currentText)
- }
- break
+ case "text":
+ if (currentText) {
+ currentText.text += value.text
+ await updatePart(currentText)
+ }
+ break
- case "text-end":
- if (currentText && currentText.text) {
- currentText.time = {
- start: Date.now(),
- end: Date.now(),
- }
- await updatePart(currentText)
- }
- currentText = undefined
- break
+ case "text-end":
+ if (currentText && currentText.text) {
+ currentText.time = {
+ start: Date.now(),
+ end: Date.now(),
+ }
+ await updatePart(currentText)
+ }
+ currentText = undefined
+ break
- case "finish":
- assistantMsg.time.completed = Date.now()
- await updateMessage(assistantMsg)
- break
+ case "finish":
+ assistantMsg.time.completed = Date.now()
+ await updateMessage(assistantMsg)
+ break
- default:
- log.info("unhandled", {
- ...value,
+ default:
+ log.info("unhandled", {
+ ...value,
+ })
+ continue
+ }
+ }
+ } catch (e) {
+ log.error("", {
+ error: e,
+ })
+ switch (true) {
+ case e instanceof DOMException && e.name === "AbortError":
+ assistantMsg.error = new MessageV2.AbortedError(
+ { message: e.message },
+ {
+ cause: e,
+ },
+ ).toObject()
+ break
+ case MessageV2.OutputLengthError.isInstance(e):
+ assistantMsg.error = e
+ break
+ case LoadAPIKeyError.isInstance(e):
+ assistantMsg.error = new MessageV2.AuthError(
+ {
+ providerID: model.id,
+ message: e.message,
+ },
+ { cause: e },
+ ).toObject()
+ break
+ case e instanceof Error:
+ assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
+ break
+ default:
+ assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
+ }
+ Bus.publish(Event.Error, {
+ sessionID: assistantMsg.sessionID,
+ error: assistantMsg.error,
+ })
+ }
+ const p = await parts(assistantMsg.sessionID, assistantMsg.id)
+ for (const part of p) {
+ if (part.type === "tool" && part.state.status !== "completed") {
+ updatePart({
+ ...part,
+ state: {
+ status: "error",
+ error: "Tool execution aborted",
+ time: {
+ start: Date.now(),
+ end: Date.now(),
+ },
+ input: {},
+ },
})
- continue
+ }
}
- }
- } catch (e) {
- log.error("", {
- error: e,
- })
- switch (true) {
- case e instanceof DOMException && e.name === "AbortError":
- assistantMsg.error = new MessageV2.AbortedError(
- { message: e.message },
- {
- cause: e,
- },
- ).toObject()
- break
- case MessageV2.OutputLengthError.isInstance(e):
- assistantMsg.error = e
- break
- case LoadAPIKeyError.isInstance(e):
- assistantMsg.error = new Provider.AuthError(
- {
- providerID: model.id,
- message: e.message,
- },
- { cause: e },
- ).toObject()
- break
- case e instanceof Error:
- assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
- break
- default:
- assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
- }
- Bus.publish(Event.Error, {
- sessionID: assistantMsg.sessionID,
- error: assistantMsg.error,
- })
- }
- const p = await parts(assistantMsg.sessionID, assistantMsg.id)
- for (const part of p) {
- if (part.type === "tool" && part.state.status !== "completed") {
- updatePart({
- ...part,
- state: {
- status: "error",
- error: "Tool execution aborted",
- time: {
- start: Date.now(),
- end: Date.now(),
- },
- input: {},
- },
- })
- }
+ assistantMsg.time.completed = Date.now()
+ await updateMessage(assistantMsg)
+ return { info: assistantMsg, parts: p }
+ },
}
- assistantMsg.time.completed = Date.now()
- await updateMessage(assistantMsg)
- return { info: assistantMsg, parts: p }
}
export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
@@ -1006,6 +1010,7 @@ export namespace Session {
}
await updateMessage(next)
+ const processor = createProcessor(next, model.info)
const stream = streamText({
abortSignal: abort.signal,
model: model.language,
@@ -1029,7 +1034,7 @@ export namespace Session {
],
})
- const result = await processStream(next, model.info, stream)
+ const result = await processor.process(stream)
return result
}
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 0031f6eac..744eaadc9 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -1,6 +1,5 @@
import z from "zod"
import { Bus } from "../bus"
-import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
@@ -9,6 +8,13 @@ import { Identifier } from "../id/id"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({}))
+ export const AuthError = NamedError.create(
+ "ProviderAuthError",
+ z.object({
+ providerID: z.string(),
+ message: z.string(),
+ }),
+ )
export const ToolStatePending = z
.object({
@@ -173,7 +179,7 @@ export namespace MessageV2 {
}),
error: z
.discriminatedUnion("name", [
- Provider.AuthError.Schema,
+ AuthError.Schema,
NamedError.Unknown.Schema,
OutputLengthError.Schema,
AbortedError.Schema,
diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts
index 34b522fc3..e71c35c5a 100644
--- a/packages/opencode/src/session/message.ts
+++ b/packages/opencode/src/session/message.ts
@@ -1,9 +1,15 @@
import z from "zod"
-import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
export namespace Message {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
+ export const AuthError = NamedError.create(
+ "ProviderAuthError",
+ z.object({
+ providerID: z.string(),
+ message: z.string(),
+ }),
+ )
export const ToolCall = z
.object({
@@ -134,11 +140,7 @@ export namespace Message {
completed: z.number().optional(),
}),
error: z
- .discriminatedUnion("name", [
- Provider.AuthError.Schema,
- NamedError.Unknown.Schema,
- OutputLengthError.Schema,
- ])
+ .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
sessionID: z.string(),
tool: z.record(
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
index 442ce82b0..97efcef7c 100644
--- a/packages/opencode/src/storage/storage.ts
+++ b/packages/opencode/src/storage/storage.ts
@@ -129,7 +129,7 @@ export namespace Storage {
cwd: path.join(dir, prefix),
onlyFiles: true,
}),
- )
+ ).then((items) => items.map((item) => path.join(prefix, item.slice(0, -5))))
result.sort()
return result
} catch {
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 0d7808a3a..49b89495f 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -15,21 +15,15 @@ export const TaskTool = Tool.define({
}),
async execute(params, ctx) {
const session = await Session.create(ctx.sessionID)
- const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
-
- const parts: Record<string, MessageV2.Part> = {}
- function summary(input: MessageV2.Part[]) {
- const result = []
- for (const part of input) {
- if (part.type === "tool" && part.state.status === "completed") {
- result.push(part)
- }
- }
- return result
- }
+ const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
+ if (msg.role !== "assistant") throw new Error("Not an assistant message")
+ const messageID = Identifier.ascending("message")
+ const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return
+ if (evt.properties.part.messageID === messageID) return
+ if (evt.properties.part.type !== "tool") return
parts[evt.properties.part.id] = evt.properties.part
ctx.metadata({
title: params.description,
@@ -42,7 +36,6 @@ export const TaskTool = Tool.define({
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
- const messageID = Identifier.ascending("message")
const result = await Session.chat({
messageID,
sessionID: session.id,
@@ -62,7 +55,7 @@ export const TaskTool = Tool.define({
return {
title: params.description,
metadata: {
- summary: summary(result.parts),
+ summary: result.parts.filter((x) => x.type === "tool"),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index b2a5d7166..f6a4604c3 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -305,10 +305,8 @@ func renderToolDetails(
return ""
}
- if toolCall.State.Status == opencode.ToolPartStateStatusPending ||
- toolCall.State.Status == opencode.ToolPartStateStatusRunning {
+ if toolCall.State.Status == opencode.ToolPartStateStatusPending {
title := renderToolTitle(toolCall, width)
- title = styles.NewStyle().Width(width - 6).Render(title)
return renderContentBlock(app, title, highlight, width)
}
@@ -339,128 +337,124 @@ func renderToolDetails(
borderColor = t.BorderActive()
}
- if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
- metadata := toolCall.State.Metadata.(map[string]any)
- switch toolCall.Tool {
- case "read":
- preview := metadata["preview"]
- if preview != nil && toolInputMap["filePath"] != nil {
- filename := toolInputMap["filePath"].(string)
- body = preview.(string)
- body = util.RenderFile(filename, body, width, util.WithTruncate(6))
- }
- case "edit":
- if filename, ok := toolInputMap["filePath"].(string); ok {
- diffField := metadata["diff"]
- if diffField != nil {
- patch := diffField.(string)
- var formattedDiff string
- formattedDiff, _ = diff.FormatUnifiedDiff(
- filename,
- patch,
- diff.WithWidth(width-2),
- )
- body = strings.TrimSpace(formattedDiff)
- style := styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.TextMuted()).
- Padding(1, 2).
- Width(width - 4)
- if highlight {
- style = style.Foreground(t.Text()).Bold(true)
- }
-
- if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
- diagnostics = style.Render(diagnostics)
- body += "\n" + diagnostics
- }
-
- title := renderToolTitle(toolCall, width)
- title = style.Render(title)
- content := title + "\n" + body
- content = renderContentBlock(
- app,
- content,
- highlight,
- width,
- WithPadding(0),
- WithBorderColor(borderColor),
- )
- return content
+ metadata := toolCall.State.Metadata.(map[string]any)
+ switch toolCall.Tool {
+ case "read":
+ preview := metadata["preview"]
+ if preview != nil && toolInputMap["filePath"] != nil {
+ filename := toolInputMap["filePath"].(string)
+ body = preview.(string)
+ body = util.RenderFile(filename, body, width, util.WithTruncate(6))
+ }
+ case "edit":
+ if filename, ok := toolInputMap["filePath"].(string); ok {
+ diffField := metadata["diff"]
+ if diffField != nil {
+ patch := diffField.(string)
+ var formattedDiff string
+ formattedDiff, _ = diff.FormatUnifiedDiff(
+ filename,
+ patch,
+ diff.WithWidth(width-2),
+ )
+ body = strings.TrimSpace(formattedDiff)
+ style := styles.NewStyle().
+ Background(backgroundColor).
+ Foreground(t.TextMuted()).
+ Padding(1, 2).
+ Width(width - 4)
+ if highlight {
+ style = style.Foreground(t.Text()).Bold(true)
}
- }
- case "write":
- if filename, ok := toolInputMap["filePath"].(string); ok {
- if content, ok := toolInputMap["content"].(string); ok {
- body = util.RenderFile(filename, content, width)
- if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
- body += "\n\n" + diagnostics
- }
+
+ if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ diagnostics = style.Render(diagnostics)
+ body += "\n" + diagnostics
}
+
+ title := renderToolTitle(toolCall, width)
+ title = style.Render(title)
+ content := title + "\n" + body
+ content = renderContentBlock(
+ app,
+ content,
+ highlight,
+ width,
+ WithPadding(0),
+ WithBorderColor(borderColor),
+ )
+ return content
}
- case "bash":
- stdout := metadata["stdout"]
- if stdout != nil {
- command := toolInputMap["command"].(string)
- body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
- body = util.ToMarkdown(body, width, backgroundColor)
- }
- case "webfetch":
- if format, ok := toolInputMap["format"].(string); ok && result != nil {
- body = *result
- body = util.TruncateHeight(body, 10)
- if format == "html" || format == "markdown" {
- body = util.ToMarkdown(body, width, backgroundColor)
+ }
+ case "write":
+ if filename, ok := toolInputMap["filePath"].(string); ok {
+ if content, ok := toolInputMap["content"].(string); ok {
+ body = util.RenderFile(filename, content, width)
+ if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
+ body += "\n\n" + diagnostics
}
}
- case "todowrite":
- todos := metadata["todos"]
- if todos != nil {
- for _, item := range todos.([]any) {
- todo := item.(map[string]any)
- content := todo["content"].(string)
- switch todo["status"] {
- case "completed":
- body += fmt.Sprintf("- [x] %s\n", content)
- case "cancelled":
- // strike through cancelled todo
- body += fmt.Sprintf("- [~] ~~%s~~\n", content)
- case "in_progress":
- // highlight in progress todo
- body += fmt.Sprintf("- [ ] `%s`\n", content)
- default:
- body += fmt.Sprintf("- [ ] %s\n", content)
- }
- }
+ }
+ case "bash":
+ stdout := metadata["stdout"]
+ if stdout != nil {
+ command := toolInputMap["command"].(string)
+ body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
+ body = util.ToMarkdown(body, width, backgroundColor)
+ }
+ case "webfetch":
+ if format, ok := toolInputMap["format"].(string); ok && result != nil {
+ body = *result
+ body = util.TruncateHeight(body, 10)
+ if format == "html" || format == "markdown" {
body = util.ToMarkdown(body, width, backgroundColor)
}
- case "task":
- summary := metadata["summary"]
- if summary != nil {
- toolcalls := summary.([]any)
- steps := []string{}
- for _, toolcall := range toolcalls {
- call := toolcall.(map[string]any)
- if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
- data, _ := json.Marshal(toolInvocation)
- var toolCall opencode.ToolPart
- _ = json.Unmarshal(data, &toolCall)
- step := renderToolTitle(toolCall, width)
- step = "∟ " + step
- steps = append(steps, step)
- }
+ }
+ case "todowrite":
+ todos := metadata["todos"]
+ if todos != nil {
+ for _, item := range todos.([]any) {
+ todo := item.(map[string]any)
+ content := todo["content"].(string)
+ switch todo["status"] {
+ case "completed":
+ body += fmt.Sprintf("- [x] %s\n", content)
+ case "cancelled":
+ // strike through cancelled todo
+ body += fmt.Sprintf("- [~] ~~%s~~\n", content)
+ case "in_progress":
+ // highlight in progress todo
+ body += fmt.Sprintf("- [ ] `%s`\n", content)
+ default:
+ body += fmt.Sprintf("- [ ] %s\n", content)
}
- body = strings.Join(steps, "\n")
}
- default:
- if result == nil {
- empty := ""
- result = &empty
+ body = util.ToMarkdown(body, width, backgroundColor)
+ }
+ case "task":
+ summary := metadata["summary"]
+ if summary != nil {
+ toolcalls := summary.([]any)
+ steps := []string{}
+ for _, item := range toolcalls {
+ data, _ := json.Marshal(item)
+ var toolCall opencode.ToolPart
+ _ = json.Unmarshal(data, &toolCall)
+ step := renderToolTitle(toolCall, width)
+ step = "∟ " + step
+ steps = append(steps, step)
}
- body = *result
- body = util.TruncateHeight(body, 10)
- body = styles.NewStyle().Width(width - 6).Render(body)
+ body = strings.Join(steps, "\n")
}
+ body = styles.NewStyle().Width(width - 6).Render(body)
+ default:
+ if result == nil {
+ empty := ""
+ result = &empty
+ }
+ body = *result
+ body = util.TruncateHeight(body, 10)
+ body = styles.NewStyle().Width(width - 6).Render(body)
}
error := ""
@@ -539,10 +533,9 @@ func renderToolTitle(
toolCall opencode.ToolPart,
width int,
) string {
- // TODO: handle truncate to width
-
if toolCall.State.Status == opencode.ToolPartStateStatusPending {
- return renderToolAction(toolCall.Tool)
+ title := renderToolAction(toolCall.Tool)
+ return styles.NewStyle().Width(width - 6).Render(title)
}
toolArgs := ""
@@ -596,7 +589,7 @@ func renderToolTitle(
func renderToolAction(name string) string {
switch name {
case "task":
- return "Searching..."
+ return "Planning..."
case "bash":
return "Writing command..."
case "edit":