fix: restore complete source code and fix launchers

- Copy complete source code packages from original CodeNomad project
- Add root package.json with npm workspace configuration
- Include electron-app, server, ui, tauri-app, and opencode-config packages
- Fix Launch-Windows.bat and Launch-Dev-Windows.bat to work with correct npm scripts
- Fix Launch-Unix.sh to work with correct npm scripts
- Launchers now correctly call npm run dev:electron which launches Electron app
This commit is contained in:
Gemini AI
2025-12-23 12:57:55 +04:00
Unverified
parent f365d64c97
commit b448d11991
249 changed files with 48776 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, formatUnknown, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, readToolStatePayload } from "../utils"
export const bashRenderer: ToolRenderer = {
tools: ["bash"],
getAction: () => "Writing command...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
const name = getToolName("bash")
const description = typeof input.description === "string" && input.description.length > 0 ? input.description : ""
const timeout = typeof input.timeout === "number" && input.timeout > 0 ? input.timeout : undefined
const baseTitle = description ? `${name} ${description}` : name
if (!timeout) {
return baseTitle
}
const timeoutLabel = `${timeout}ms`
return `${baseTitle} · Timeout: ${timeoutLabel}`
},
renderBody({ toolState, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const { input, metadata } = readToolStatePayload(state)
const command = typeof input.command === "string" && input.command.length > 0 ? `$ ${input.command}` : ""
const outputResult = formatUnknown(
isToolStateCompleted(state)
? state.output
: (isToolStateRunning(state) || isToolStateError(state)) && metadata.output
? metadata.output
: undefined,
)
const parts = [command, outputResult?.text].filter(Boolean)
if (parts.length === 0) return null
const content = ensureMarkdownContent(parts.join("\n"), "bash", true)
if (!content) return null
return renderMarkdown({ content, disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,25 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, formatUnknown, isToolStateCompleted, isToolStateError, isToolStateRunning, readToolStatePayload } from "../utils"
export const defaultRenderer: ToolRenderer = {
tools: ["*"],
renderBody({ toolState, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const { metadata, input } = readToolStatePayload(state)
const primaryOutput = isToolStateCompleted(state)
? state.output
: (isToolStateRunning(state) || isToolStateError(state)) && metadata.output
? metadata.output
: metadata.diff ?? metadata.preview ?? input.content
const result = formatUnknown(primaryOutput)
if (!result) return null
const content = ensureMarkdownContent(result.text, result.language, true)
if (!content) return null
return renderMarkdown({ content, disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,32 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, extractDiffPayload, getRelativePath, getToolName, isToolStateCompleted, readToolStatePayload } from "../utils"
export const editRenderer: ToolRenderer = {
tools: ["edit"],
getAction: () => "Preparing edit...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
const filePath = typeof input.filePath === "string" ? input.filePath : ""
if (!filePath) return getToolName("edit")
return `${getToolName("edit")} ${getRelativePath(filePath)}`
},
renderBody({ toolState, toolName, renderDiff, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const diffPayload = extractDiffPayload(toolName(), state)
if (diffPayload) {
return renderDiff(diffPayload)
}
const { metadata } = readToolStatePayload(state)
const diffText = typeof metadata.diff === "string" ? metadata.diff : null
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
const content = ensureMarkdownContent(diffText || fallback, "diff", true)
if (!content) return null
return renderMarkdown({ content, size: "large", disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,36 @@
import type { ToolRenderer } from "../types"
import { bashRenderer } from "./bash"
import { defaultRenderer } from "./default"
import { editRenderer } from "./edit"
import { patchRenderer } from "./patch"
import { readRenderer } from "./read"
import { taskRenderer } from "./task"
import { todoRenderer } from "./todo"
import { webfetchRenderer } from "./webfetch"
import { writeRenderer } from "./write"
import { invalidRenderer } from "./invalid"
const TOOL_RENDERERS: ToolRenderer[] = [
bashRenderer,
readRenderer,
writeRenderer,
editRenderer,
patchRenderer,
webfetchRenderer,
todoRenderer,
taskRenderer,
invalidRenderer,
]
const rendererMap = TOOL_RENDERERS.reduce<Record<string, ToolRenderer>>((acc, renderer) => {
renderer.tools.forEach((tool) => {
acc[tool] = renderer
})
return acc
}, {})
export function resolveToolRenderer(toolName: string): ToolRenderer {
return rendererMap[toolName] ?? defaultRenderer
}
export { defaultRenderer }

View File

@@ -0,0 +1,19 @@
import type { ToolRenderer } from "../types"
import { defaultRenderer } from "./default"
import { getToolName, readToolStatePayload } from "../utils"
export const invalidRenderer: ToolRenderer = {
tools: ["invalid"],
getTitle({ toolState }) {
const state = toolState()
if (!state) return getToolName("invalid")
const { input } = readToolStatePayload(state)
if (typeof input.tool === "string") {
return getToolName(input.tool)
}
return getToolName("invalid")
},
renderBody(context) {
return defaultRenderer.renderBody(context)
},
}

View File

@@ -0,0 +1,32 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, extractDiffPayload, getRelativePath, getToolName, isToolStateCompleted, readToolStatePayload } from "../utils"
export const patchRenderer: ToolRenderer = {
tools: ["patch"],
getAction: () => "Preparing patch...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
const filePath = typeof input.filePath === "string" ? input.filePath : ""
if (!filePath) return getToolName("patch")
return `${getToolName("patch")} ${getRelativePath(filePath)}`
},
renderBody({ toolState, toolName, renderDiff, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const diffPayload = extractDiffPayload(toolName(), state)
if (diffPayload) {
return renderDiff(diffPayload)
}
const { metadata } = readToolStatePayload(state)
const diffText = typeof metadata.diff === "string" ? metadata.diff : null
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
const content = ensureMarkdownContent(diffText || fallback, "diff", true)
if (!content) return null
return renderMarkdown({ content, size: "large", disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,42 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, getRelativePath, getToolName, inferLanguageFromPath, readToolStatePayload } from "../utils"
export const readRenderer: ToolRenderer = {
tools: ["read"],
getAction: () => "Reading file...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
const filePath = typeof input.filePath === "string" ? input.filePath : ""
const offset = typeof input.offset === "number" ? input.offset : undefined
const limit = typeof input.limit === "number" ? input.limit : undefined
const relativePath = filePath ? getRelativePath(filePath) : ""
const detailParts: string[] = []
if (typeof offset === "number") {
detailParts.push(`Offset: ${offset}`)
}
if (typeof limit === "number") {
detailParts.push(`Limit: ${limit}`)
}
const baseTitle = relativePath ? `${getToolName("read")} ${relativePath}` : getToolName("read")
if (!detailParts.length) {
return baseTitle
}
return `${baseTitle} · ${detailParts.join(" · ")}`
},
renderBody({ toolState, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const { metadata, input } = readToolStatePayload(state)
const preview = typeof metadata.preview === "string" ? metadata.preview : null
const language = inferLanguageFromPath(typeof input.filePath === "string" ? input.filePath : undefined)
const content = ensureMarkdownContent(preview, language, true)
if (!content) return null
return renderMarkdown({ content, disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,155 @@
import { For, Show, createMemo } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import type { ToolRenderer } from "../types"
import { getDefaultToolAction, getToolIcon, getToolName, readToolStatePayload } from "../utils"
import { getTodoTitle } from "./todo"
import { resolveTitleForTool } from "../tool-title"
interface TaskSummaryItem {
id: string
tool: string
input: Record<string, any>
metadata: Record<string, any>
state?: ToolState
status?: ToolState["status"]
title?: string
}
function normalizeStatus(status?: string | null): ToolState["status"] | undefined {
if (status === "pending" || status === "running" || status === "completed" || status === "error") {
return status
}
return undefined
}
function summarizeStatusIcon(status?: ToolState["status"]) {
switch (status) {
case "pending":
return "⏸"
case "running":
return "⏳"
case "completed":
return "✓"
case "error":
return "✗"
default:
return ""
}
}
function summarizeStatusLabel(status?: ToolState["status"]) {
switch (status) {
case "pending":
return "Pending"
case "running":
return "Running"
case "completed":
return "Completed"
case "error":
return "Error"
default:
return "Unknown"
}
}
function describeTaskTitle(input: Record<string, any>) {
const description = typeof input.description === "string" ? input.description : undefined
const subagent = typeof input.subagent_type === "string" ? input.subagent_type : undefined
const base = getToolName("task")
if (description && subagent) {
return `${base}[${subagent}] ${description}`
}
if (description) {
return `${base} ${description}`
}
return base
}
function describeToolTitle(item: TaskSummaryItem): string {
if (item.title && item.title.length > 0) {
return item.title
}
if (item.tool === "task") {
return describeTaskTitle({ ...item.metadata, ...item.input })
}
if (item.state) {
return resolveTitleForTool({ toolName: item.tool, state: item.state })
}
return getDefaultToolAction(item.tool)
}
export const taskRenderer: ToolRenderer = {
tools: ["task"],
getAction: () => "Delegating...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
return describeTaskTitle(input)
},
renderBody({ toolState, messageVersion, partVersion, scrollHelpers }) {
const items = createMemo(() => {
// Track the reactive change points so we only recompute when the part/message changes
messageVersion?.()
partVersion?.()
const state = toolState()
if (!state) return []
const { metadata } = readToolStatePayload(state)
const summary = Array.isArray((metadata as any).summary) ? ((metadata as any).summary as any[]) : []
return summary.map((entry, index) => {
const tool = typeof entry?.tool === "string" ? (entry.tool as string) : "unknown"
const stateValue = typeof entry?.state === "object" ? (entry.state as ToolState) : undefined
const metadataFromEntry = typeof entry?.metadata === "object" && entry.metadata ? entry.metadata : {}
const fallbackInput = typeof entry?.input === "object" && entry.input ? entry.input : {}
const id = typeof entry?.id === "string" && entry.id.length > 0 ? entry.id : `${tool}-${index}`
const statusValue = normalizeStatus((entry?.status as string | undefined) ?? stateValue?.status)
const title = typeof entry?.title === "string" ? entry.title : undefined
return { id, tool, input: fallbackInput, metadata: metadataFromEntry, state: stateValue, status: statusValue, title }
})
})
if (items().length === 0) return null
return (
<div
class="message-text tool-call-markdown tool-call-task-container"
ref={(element) => scrollHelpers?.registerContainer(element)}
onScroll={scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined}
>
<div class="tool-call-task-summary">
<For each={items()}>
{(item) => {
const icon = getToolIcon(item.tool)
const description = describeToolTitle(item)
const toolLabel = getToolName(item.tool)
const status = normalizeStatus(item.status ?? item.state?.status)
const statusIcon = summarizeStatusIcon(status)
const statusLabel = summarizeStatusLabel(status)
const statusAttr = status ?? "pending"
return (
<div class="tool-call-task-item" data-task-id={item.id} data-task-status={statusAttr}>
<span class="tool-call-task-icon">{icon}</span>
<span class="tool-call-task-label">{toolLabel}</span>
<span class="tool-call-task-separator" aria-hidden="true"></span>
<span class="tool-call-task-text">{description}</span>
<Show when={statusIcon}>
<span class="tool-call-task-status" aria-label={statusLabel} title={statusLabel}>
{statusIcon}
</span>
</Show>
</div>
)
}}
</For>
</div>
{scrollHelpers?.renderSentinel?.()}
</div>
)
},
}

View File

@@ -0,0 +1,134 @@
import { For, Show } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import type { ToolRenderer } from "../types"
import { readToolStatePayload } from "../utils"
export type TodoViewStatus = "pending" | "in_progress" | "completed" | "cancelled"
export interface TodoViewItem {
id: string
content: string
status: TodoViewStatus
}
function normalizeTodoStatus(rawStatus: unknown): TodoViewStatus {
if (rawStatus === "completed" || rawStatus === "in_progress" || rawStatus === "cancelled") return rawStatus
return "pending"
}
function extractTodosFromState(state?: ToolState): TodoViewItem[] {
if (!state) return []
const { metadata } = readToolStatePayload(state)
const todos = Array.isArray((metadata as any).todos) ? (metadata as any).todos : []
const items: TodoViewItem[] = []
for (let index = 0; index < todos.length; index++) {
const todo = todos[index]
const content = typeof todo?.content === "string" ? todo.content.trim() : ""
if (!content) continue
const status = normalizeTodoStatus((todo as any).status)
const id = typeof todo?.id === "string" && todo.id.length > 0 ? todo.id : `${index}-${content}`
items.push({ id, content, status })
}
return items
}
function summarizeTodos(todos: TodoViewItem[]) {
return todos.reduce(
(acc, todo) => {
acc.total += 1
acc[todo.status] = (acc[todo.status] || 0) + 1
return acc
},
{ total: 0, pending: 0, in_progress: 0, completed: 0, cancelled: 0 } as Record<TodoViewStatus | "total", number>,
)
}
function getTodoStatusLabel(status: TodoViewStatus): string {
switch (status) {
case "completed":
return "Completed"
case "in_progress":
return "In progress"
case "cancelled":
return "Cancelled"
default:
return "Pending"
}
}
interface TodoListViewProps {
state?: ToolState
emptyLabel?: string
showStatusLabel?: boolean
}
export function TodoListView(props: TodoListViewProps) {
const todos = extractTodosFromState(props.state)
const counts = summarizeTodos(todos)
if (counts.total === 0) {
return <div class="tool-call-todo-empty">{props.emptyLabel ?? "No plan items yet."}</div>
}
return (
<div class="tool-call-todo-region">
<div class="tool-call-todos" role="list">
<For each={todos}>
{(todo) => {
const label = getTodoStatusLabel(todo.status)
return (
<div
class="tool-call-todo-item"
classList={{
"tool-call-todo-item-completed": todo.status === "completed",
"tool-call-todo-item-cancelled": todo.status === "cancelled",
"tool-call-todo-item-active": todo.status === "in_progress",
}}
role="listitem"
>
<span class="tool-call-todo-checkbox" data-status={todo.status} aria-label={label}></span>
<div class="tool-call-todo-body">
<div class="tool-call-todo-heading">
<span class="tool-call-todo-text">{todo.content}</span>
<Show when={props.showStatusLabel !== false}>
<span class={`tool-call-todo-status tool-call-todo-status-${todo.status}`}>{label}</span>
</Show>
</div>
</div>
</div>
)
}}
</For>
</div>
</div>
)
}
export function getTodoTitle(state?: ToolState): string {
if (!state) return "Plan"
const todos = extractTodosFromState(state)
if (state.status !== "completed" || todos.length === 0) return "Plan"
const counts = summarizeTodos(todos)
if (counts.pending === counts.total) return "Creating plan"
if (counts.completed === counts.total) return "Completing plan"
return "Updating plan"
}
export const todoRenderer: ToolRenderer = {
tools: ["todowrite", "todoread"],
getAction: () => "Planning...",
getTitle({ toolState }) {
return getTodoTitle(toolState())
},
renderBody({ toolState }) {
const state = toolState()
if (!state) return null
return <TodoListView state={state} />
},
}

View File

@@ -0,0 +1,33 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, formatUnknown, getToolName, readToolStatePayload } from "../utils"
export const webfetchRenderer: ToolRenderer = {
tools: ["webfetch"],
getAction: () => "Fetching from the web...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
if (typeof input.url === "string" && input.url.length > 0) {
return `${getToolName("webfetch")} ${input.url}`
}
return getToolName("webfetch")
},
renderBody({ toolState, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const { metadata } = readToolStatePayload(state)
const result = formatUnknown(
state.status === "completed"
? state.output
: metadata.output,
)
if (!result) return null
const content = ensureMarkdownContent(result.text, result.language, true)
if (!content) return null
return renderMarkdown({ content, disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,25 @@
import type { ToolRenderer } from "../types"
import { ensureMarkdownContent, getRelativePath, getToolName, inferLanguageFromPath, readToolStatePayload } from "../utils"
export const writeRenderer: ToolRenderer = {
tools: ["write"],
getAction: () => "Preparing write...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return undefined
const { input } = readToolStatePayload(state)
const filePath = typeof input.filePath === "string" ? input.filePath : ""
if (!filePath) return getToolName("write")
return `${getToolName("write")} ${getRelativePath(filePath)}`
},
renderBody({ toolState, renderMarkdown }) {
const state = toolState()
if (!state || state.status === "pending") return null
const { metadata, input } = readToolStatePayload(state)
const contentValue = typeof input.content === "string" ? input.content : metadata.content
const filePath = typeof input.filePath === "string" ? input.filePath : undefined
const content = ensureMarkdownContent(contentValue ?? null, inferLanguageFromPath(filePath), true)
if (!content) return null
return renderMarkdown({ content, size: "large", disableHighlight: state.status === "running" })
},
}

View File

@@ -0,0 +1,86 @@
import type { ToolState } from "@opencode-ai/sdk"
import type { ToolRendererContext, ToolRenderer, ToolCallPart } from "./types"
import { getDefaultToolAction, getToolName, isToolStateCompleted, isToolStateRunning } from "./utils"
import { defaultRenderer } from "./renderers/default"
import { bashRenderer } from "./renderers/bash"
import { readRenderer } from "./renderers/read"
import { writeRenderer } from "./renderers/write"
import { editRenderer } from "./renderers/edit"
import { patchRenderer } from "./renderers/patch"
import { webfetchRenderer } from "./renderers/webfetch"
import { todoRenderer } from "./renderers/todo"
import { invalidRenderer } from "./renderers/invalid"
const TITLE_RENDERERS: Record<string, ToolRenderer> = {
bash: bashRenderer,
read: readRenderer,
write: writeRenderer,
edit: editRenderer,
patch: patchRenderer,
webfetch: webfetchRenderer,
todowrite: todoRenderer,
todoread: todoRenderer,
invalid: invalidRenderer,
}
interface TitleSnapshot {
toolName: string
state?: ToolState
}
function lookupRenderer(toolName: string): ToolRenderer {
return TITLE_RENDERERS[toolName] ?? defaultRenderer
}
function createStaticToolPart(snapshot: TitleSnapshot): ToolCallPart {
return {
id: "",
type: "tool",
tool: snapshot.toolName,
state: snapshot.state,
} as ToolCallPart
}
function createStaticContext(snapshot: TitleSnapshot): ToolRendererContext {
const toolStateAccessor = () => snapshot.state
const toolNameAccessor = () => snapshot.toolName
const toolCallAccessor = () => createStaticToolPart(snapshot)
const messageVersionAccessor = () => undefined
const partVersionAccessor = () => undefined
const renderMarkdown: ToolRendererContext["renderMarkdown"] = () => null
const renderDiff: ToolRendererContext["renderDiff"] = () => null
return {
toolCall: toolCallAccessor,
toolState: toolStateAccessor,
toolName: toolNameAccessor,
messageVersion: messageVersionAccessor,
partVersion: partVersionAccessor,
renderMarkdown,
renderDiff,
scrollHelpers: undefined,
}
}
export function resolveTitleForTool(snapshot: TitleSnapshot): string {
const renderer = lookupRenderer(snapshot.toolName)
const context = createStaticContext(snapshot)
const state = snapshot.state
const defaultAction = renderer.getAction?.(context) ?? getDefaultToolAction(snapshot.toolName)
if (!state || state.status === "pending") {
return defaultAction
}
const stateTitle = typeof (state as { title?: string }).title === "string" ? (state as { title?: string }).title : undefined
if (stateTitle && stateTitle.length > 0) {
return stateTitle
}
const customTitle = renderer.getTitle?.(context)
if (customTitle) {
return customTitle
}
return getToolName(snapshot.toolName)
}

View File

@@ -0,0 +1,48 @@
import type { Accessor, JSXElement } from "solid-js"
import type { ToolState } from "@opencode-ai/sdk"
import type { ClientPart } from "../../types/message"
export type ToolCallPart = Extract<ClientPart, { type: "tool" }>
export interface DiffPayload {
diffText: string
filePath?: string
}
export interface MarkdownRenderOptions {
content: string
size?: "default" | "large"
disableHighlight?: boolean
}
export interface DiffRenderOptions {
variant?: string
disableScrollTracking?: boolean
label?: string
}
export interface ToolScrollHelpers {
registerContainer(element: HTMLDivElement | null, options?: { disableTracking?: boolean }): void
handleScroll(event: Event & { currentTarget: HTMLDivElement }): void
renderSentinel(options?: { disableTracking?: boolean }): JSXElement | null
}
export interface ToolRendererContext {
toolCall: Accessor<ToolCallPart>
toolState: Accessor<ToolState | undefined>
toolName: Accessor<string>
messageVersion?: Accessor<number | undefined>
partVersion?: Accessor<number | undefined>
renderMarkdown(options: MarkdownRenderOptions): JSXElement | null
renderDiff(payload: DiffPayload, options?: DiffRenderOptions): JSXElement | null
scrollHelpers?: ToolScrollHelpers
}
export interface ToolRenderer {
tools: string[]
getTitle?(context: ToolRendererContext): string | undefined
getAction?(context: ToolRendererContext): string | undefined
renderBody(context: ToolRendererContext): JSXElement | null
}
export type ToolRendererMap = Record<string, ToolRenderer>

View File

@@ -0,0 +1,224 @@
import { isRenderableDiffText } from "../../lib/diff-utils"
import { getLanguageFromPath } from "../../lib/markdown"
import type { ToolState } from "@opencode-ai/sdk"
import type { DiffPayload } from "./types"
import { getLogger } from "../../lib/logger"
const log = getLogger("session")
export type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
export type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
export type ToolStateError = import("@opencode-ai/sdk").ToolStateError
export const diffCapableTools = new Set(["edit", "patch"])
export function isToolStateRunning(state: ToolState): state is ToolStateRunning {
return state.status === "running"
}
export function isToolStateCompleted(state: ToolState): state is ToolStateCompleted {
return state.status === "completed"
}
export function isToolStateError(state: ToolState): state is ToolStateError {
return state.status === "error"
}
export function getToolIcon(tool: string): string {
switch (tool) {
case "bash":
return "⚡"
case "edit":
return "✏️"
case "read":
return "📖"
case "write":
return "📝"
case "glob":
return "🔍"
case "grep":
return "🔎"
case "webfetch":
return "🌐"
case "task":
return "🎯"
case "todowrite":
case "todoread":
return "📋"
case "list":
return "📁"
case "patch":
return "🔧"
default:
return "🔧"
}
}
export function getToolName(tool: string): string {
switch (tool) {
case "bash":
return "Shell"
case "webfetch":
return "Fetch"
case "invalid":
return "Invalid"
case "todowrite":
case "todoread":
return "Plan"
default: {
const normalized = tool.replace(/^opencode_/, "")
return normalized.charAt(0).toUpperCase() + normalized.slice(1)
}
}
}
export function getRelativePath(path: string): string {
if (!path) return ""
const parts = path.split("/")
return parts.slice(-1)[0] || path
}
export function ensureMarkdownContent(
value: string | null,
language?: string,
forceFence = false,
): string | null {
if (!value) {
return null
}
const trimmed = value.replace(/\s+$/, "")
if (!trimmed) {
return null
}
const startsWithFence = trimmed.trimStart().startsWith("```")
if (startsWithFence && !forceFence) {
return trimmed
}
const langSuffix = language ? language : ""
if (language || forceFence) {
return `\u0060\u0060\u0060${langSuffix}\n${trimmed}\n\u0060\u0060\u0060`
}
return trimmed
}
export function formatUnknown(value: unknown): { text: string; language?: string } | null {
if (value === null || value === undefined) {
return null
}
if (typeof value === "string") {
return { text: value }
}
if (typeof value === "number" || typeof value === "boolean") {
return { text: String(value) }
}
if (Array.isArray(value)) {
const parts = value
.map((item) => {
const formatted = formatUnknown(item)
return formatted?.text ?? ""
})
.filter(Boolean)
if (parts.length === 0) {
return null
}
return { text: parts.join("\n") }
}
if (typeof value === "object") {
try {
return { text: JSON.stringify(value, null, 2), language: "json" }
} catch (error) {
log.error("Failed to stringify tool call output", error)
return { text: String(value) }
}
}
return null
}
export function inferLanguageFromPath(path?: string): string | undefined {
return getLanguageFromPath(path || "")
}
export function extractDiffPayload(toolName: string, state?: ToolState): DiffPayload | null {
if (!state) return null
if (!diffCapableTools.has(toolName)) return null
const { metadata, input, output } = readToolStatePayload(state)
const candidates = [metadata.diff, output, metadata.output]
let diffText: string | null = null
for (const candidate of candidates) {
if (typeof candidate === "string" && isRenderableDiffText(candidate)) {
diffText = candidate
break
}
}
if (!diffText) {
return null
}
const filePath =
(typeof input.filePath === "string" ? input.filePath : undefined) ||
(typeof metadata.filePath === "string" ? metadata.filePath : undefined) ||
(typeof input.path === "string" ? input.path : undefined)
return { diffText, filePath }
}
export function readToolStatePayload(state?: ToolState): {
input: Record<string, any>
metadata: Record<string, any>
output: unknown
} {
if (!state) {
return { input: {}, metadata: {}, output: undefined }
}
const supportsMetadata = isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state)
return {
input: supportsMetadata ? ((state.input || {}) as Record<string, any>) : {},
metadata: supportsMetadata ? ((state.metadata || {}) as Record<string, any>) : {},
output: isToolStateCompleted(state) ? state.output : undefined,
}
}
export function getDefaultToolAction(toolName: string) {
switch (toolName) {
case "task":
return "Delegating..."
case "bash":
return "Writing command..."
case "edit":
return "Preparing edit..."
case "webfetch":
return "Fetching from the web..."
case "glob":
return "Finding files..."
case "grep":
return "Searching content..."
case "list":
return "Listing directory..."
case "read":
return "Reading file..."
case "write":
return "Preparing write..."
case "todowrite":
case "todoread":
return "Planning..."
case "patch":
return "Preparing patch..."
default:
return "Working..."
}
}