Compare commits
7 Commits
codenomad/
...
v0.11.3-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2124e540aa | ||
|
|
b5790998b7 | ||
|
|
9800afb785 | ||
|
|
3b73d9d5b9 | ||
|
|
f7ac30afe3 | ||
|
|
ce370d5100 | ||
|
|
c639e535b5 |
3
package-lock.json
generated
3
package-lock.json
generated
@@ -12092,7 +12092,8 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0"
|
"tauri-plugin-keepawake-api": "^0.1.0",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { FileSystemBrowser } from "../filesystem/browser"
|
|||||||
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
|
import { searchWorkspaceFiles, WorkspaceFileSearchOptions } from "../filesystem/search"
|
||||||
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
|
import { clearWorkspaceSearchCache } from "../filesystem/search-cache"
|
||||||
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
|
import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types"
|
||||||
import { WorkspaceRuntime, ProcessExitInfo, probeBinaryVersion } from "./runtime"
|
import { WorkspaceRuntime, ProcessExitInfo } from "./runtime"
|
||||||
import { Logger } from "../logger"
|
import { Logger } from "../logger"
|
||||||
import { getOpencodeConfigDir } from "../opencode-config.js"
|
import { getOpencodeConfigDir } from "../opencode-config.js"
|
||||||
import {
|
import {
|
||||||
@@ -109,10 +109,6 @@ export class WorkspaceManager {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!descriptor.binaryVersion) {
|
|
||||||
descriptor.binaryVersion = this.detectBinaryVersion(resolvedBinaryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.workspaces.set(id, descriptor)
|
this.workspaces.set(id, descriptor)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +145,10 @@ export class WorkspaceManager {
|
|||||||
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
onExit: (info) => this.handleProcessExit(info.workspaceId, info),
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
|
const runtimeVersion = await this.waitForWorkspaceReadiness({ workspaceId: id, port, exitPromise, getLastOutput })
|
||||||
|
if (runtimeVersion) {
|
||||||
|
descriptor.binaryVersion = runtimeVersion
|
||||||
|
}
|
||||||
|
|
||||||
descriptor.pid = pid
|
descriptor.pid = pid
|
||||||
descriptor.port = port
|
descriptor.port = port
|
||||||
@@ -278,36 +277,12 @@ export class WorkspaceManager {
|
|||||||
return candidates[0] ?? ""
|
return candidates[0] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private detectBinaryVersion(resolvedPath: string): string | undefined {
|
|
||||||
if (!resolvedPath) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = probeBinaryVersion(resolvedPath)
|
|
||||||
if (result.valid) {
|
|
||||||
if (result.version) {
|
|
||||||
this.options.logger.debug({ binary: resolvedPath, version: result.version }, "Detected binary version")
|
|
||||||
return result.version
|
|
||||||
}
|
|
||||||
if (result.reported) {
|
|
||||||
this.options.logger.debug({ binary: resolvedPath, reported: result.reported }, "Binary reported version string")
|
|
||||||
return result.reported
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
this.options.logger.warn({ binary: resolvedPath, err: result.error }, "Failed to detect binary version")
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
private async waitForWorkspaceReadiness(params: {
|
private async waitForWorkspaceReadiness(params: {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
port: number
|
port: number
|
||||||
exitPromise: Promise<ProcessExitInfo>
|
exitPromise: Promise<ProcessExitInfo>
|
||||||
getLastOutput: () => string
|
getLastOutput: () => string
|
||||||
}) {
|
}): Promise<string | undefined> {
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.waitForPortAvailability(params.port),
|
this.waitForPortAvailability(params.port),
|
||||||
@@ -321,7 +296,7 @@ export class WorkspaceManager {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await this.waitForInstanceHealth(params)
|
const version = await this.waitForInstanceHealth(params)
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
this.delay(STARTUP_STABILITY_DELAY_MS),
|
this.delay(STARTUP_STABILITY_DELAY_MS),
|
||||||
@@ -334,6 +309,8 @@ export class WorkspaceManager {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForInstanceHealth(params: {
|
private async waitForInstanceHealth(params: {
|
||||||
@@ -341,7 +318,7 @@ export class WorkspaceManager {
|
|||||||
port: number
|
port: number
|
||||||
exitPromise: Promise<ProcessExitInfo>
|
exitPromise: Promise<ProcessExitInfo>
|
||||||
getLastOutput: () => string
|
getLastOutput: () => string
|
||||||
}) {
|
}): Promise<string | undefined> {
|
||||||
const probeResult = await Promise.race([
|
const probeResult = await Promise.race([
|
||||||
this.probeInstance(params.workspaceId, params.port),
|
this.probeInstance(params.workspaceId, params.port),
|
||||||
params.exitPromise.then((info) => {
|
params.exitPromise.then((info) => {
|
||||||
@@ -355,7 +332,7 @@ export class WorkspaceManager {
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (probeResult.ok) {
|
if (probeResult.ok) {
|
||||||
return
|
return probeResult.version
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestOutput = params.getLastOutput().trim()
|
const latestOutput = params.getLastOutput().trim()
|
||||||
@@ -366,8 +343,11 @@ export class WorkspaceManager {
|
|||||||
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
|
throw new Error(`Workspace ${params.workspaceId} failed health check: ${reason}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async probeInstance(workspaceId: string, port: number): Promise<{ ok: boolean; reason?: string }> {
|
private async probeInstance(
|
||||||
const url = `http://127.0.0.1:${port}/project/current`
|
workspaceId: string,
|
||||||
|
port: number,
|
||||||
|
): Promise<{ ok: boolean; reason?: string; version?: string }> {
|
||||||
|
const url = `http://127.0.0.1:${port}/global/health`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
@@ -378,11 +358,22 @@ export class WorkspaceManager {
|
|||||||
|
|
||||||
const response = await fetch(url, { headers })
|
const response = await fetch(url, { headers })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const reason = `health probe returned HTTP ${response.status}`
|
const reason = `/global/health returned HTTP ${response.status}`
|
||||||
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
|
this.options.logger.debug({ workspaceId, status: response.status }, "Health probe returned server error")
|
||||||
return { ok: false, reason }
|
return { ok: false, reason }
|
||||||
}
|
}
|
||||||
return { ok: true }
|
|
||||||
|
const payload = (await response.json().catch(() => null)) as null | { healthy?: unknown; version?: unknown }
|
||||||
|
const healthy = payload?.healthy === true
|
||||||
|
const version = typeof payload?.version === "string" ? payload.version.trim() : undefined
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
const reason = "Instance reported unhealthy"
|
||||||
|
this.options.logger.debug({ workspaceId, payload }, "Health probe returned unhealthy response")
|
||||||
|
return { ok: false, reason }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, version: version || undefined }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = error instanceof Error ? error.message : String(error)
|
const reason = error instanceof Error ? error.message : String(error)
|
||||||
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")
|
this.options.logger.debug({ workspaceId, err: error }, "Health probe failed")
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0",
|
"solid-toast": "^0.5.0",
|
||||||
"tauri-plugin-keepawake-api": "^0.1.0"
|
"tauri-plugin-keepawake-api": "^0.1.0",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { useTheme } from "./lib/theme"
|
|||||||
import { useCommands } from "./lib/hooks/use-commands"
|
import { useCommands } from "./lib/hooks/use-commands"
|
||||||
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
|
||||||
import { getLogger } from "./lib/logger"
|
import { getLogger } from "./lib/logger"
|
||||||
|
import { launchError, showLaunchError, clearLaunchError } from "./stores/launch-errors"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "./lib/launch-errors"
|
||||||
import { initReleaseNotifications } from "./stores/releases"
|
import { initReleaseNotifications } from "./stores/releases"
|
||||||
import { runtimeEnv } from "./lib/runtime-env"
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import { useI18n } from "./lib/i18n"
|
import { useI18n } from "./lib/i18n"
|
||||||
@@ -72,14 +74,9 @@ const App: Component = () => {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||||
interface LaunchErrorState {
|
|
||||||
message: string
|
|
||||||
binaryPath: string
|
|
||||||
missingBinary: boolean
|
|
||||||
}
|
|
||||||
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
|
||||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
||||||
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||||
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
||||||
@@ -244,35 +241,6 @@ const App: Component = () => {
|
|||||||
|
|
||||||
const launchErrorMessage = () => launchError()?.message ?? ""
|
const launchErrorMessage = () => launchError()?.message ?? ""
|
||||||
|
|
||||||
const formatLaunchErrorMessage = (error: unknown): string => {
|
|
||||||
if (!error) {
|
|
||||||
return t("app.launchError.fallbackMessage")
|
|
||||||
}
|
|
||||||
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (parsed && typeof parsed.error === "string") {
|
|
||||||
return parsed.error
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore JSON parse errors
|
|
||||||
}
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMissingBinaryMessage = (message: string): boolean => {
|
|
||||||
const normalized = message.toLowerCase()
|
|
||||||
return (
|
|
||||||
normalized.includes("opencode binary not found") ||
|
|
||||||
normalized.includes("binary not found") ||
|
|
||||||
normalized.includes("no such file or directory") ||
|
|
||||||
normalized.includes("binary is not executable") ||
|
|
||||||
normalized.includes("enoent")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearLaunchError = () => setLaunchError(null)
|
|
||||||
|
|
||||||
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
||||||
if (!folderPath) {
|
if (!folderPath) {
|
||||||
return
|
return
|
||||||
@@ -291,13 +259,9 @@ const App: Component = () => {
|
|||||||
port: instances().get(instanceId)?.port,
|
port: instances().get(instanceId)?.port,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = formatLaunchErrorMessage(error)
|
const message = formatLaunchErrorMessage(error, t("app.launchError.fallbackMessage"))
|
||||||
const missingBinary = isMissingBinaryMessage(message)
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
setLaunchError({
|
showLaunchError({ source: "create", message, binaryPath: selectedBinary, missingBinary })
|
||||||
message,
|
|
||||||
binaryPath: selectedBinary,
|
|
||||||
missingBinary,
|
|
||||||
})
|
|
||||||
log.error("Failed to create instance", error)
|
log.error("Failed to create instance", error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSelectingFolder(false)
|
setIsSelectingFolder(false)
|
||||||
@@ -402,6 +366,7 @@ const App: Component = () => {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
handleNewInstanceRequest,
|
handleNewInstanceRequest,
|
||||||
handleCloseInstance,
|
handleCloseInstance,
|
||||||
handleNewSession,
|
handleNewSession,
|
||||||
|
|||||||
@@ -116,11 +116,8 @@ const AlertDialog: Component = () => {
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class="modal-overlay" />
|
<Dialog.Overlay class="modal-overlay" />
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<Dialog.Content
|
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||||
class="modal-surface w-full max-w-xl md:max-w-2xl p-6 border border-base shadow-2xl max-h-[85vh] overflow-hidden flex flex-col"
|
<div class="flex items-start gap-3">
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
<div class="flex items-start gap-3 min-h-0">
|
|
||||||
<div
|
<div
|
||||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
||||||
style={{
|
style={{
|
||||||
@@ -132,16 +129,11 @@ const AlertDialog: Component = () => {
|
|||||||
>
|
>
|
||||||
{accent.symbol}
|
{accent.symbol}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0 min-h-0">
|
<div class="flex-1 min-w-0">
|
||||||
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
||||||
<Dialog.Description class="text-sm text-secondary mt-1">
|
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line break-words">
|
||||||
<div
|
{payload.message}
|
||||||
class="max-h-[60vh] overflow-auto pr-2 whitespace-pre-wrap break-words"
|
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
||||||
style={{ "overflow-wrap": "anywhere" }}
|
|
||||||
>
|
|
||||||
{payload.message}
|
|
||||||
{payload.detail && <div class="mt-3">{payload.detail}</div>}
|
|
||||||
</div>
|
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -351,7 +351,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
const blockquote = lines.map((line) => `> ${line}`).join("\n")
|
||||||
if (!blockquote) return
|
if (!blockquote) return
|
||||||
|
|
||||||
insertBlockContent(`${blockquote}\n`)
|
// End the blockquote with a blank line so the user's next line
|
||||||
|
// doesn't get parsed as a lazy continuation of the quote.
|
||||||
|
insertBlockContent(`${blockquote}\n\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertCodeSelection(rawText: string) {
|
function insertCodeSelection(rawText: string) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
|
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { Copy } from "lucide-solid"
|
import { ArrowRightSquare, Copy } from "lucide-solid"
|
||||||
|
import { stringify as stringifyYaml } from "yaml"
|
||||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||||
import { useTheme } from "../lib/theme"
|
import { useTheme } from "../lib/theme"
|
||||||
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
import { useGlobalCache } from "../lib/hooks/use-global-cache"
|
||||||
@@ -27,7 +28,17 @@ import type {
|
|||||||
ToolRendererContext,
|
ToolRendererContext,
|
||||||
ToolScrollHelpers,
|
ToolScrollHelpers,
|
||||||
} from "./tool-call/types"
|
} from "./tool-call/types"
|
||||||
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning, getDefaultToolAction } from "./tool-call/utils"
|
import {
|
||||||
|
ensureMarkdownContent,
|
||||||
|
getRelativePath,
|
||||||
|
getToolIcon,
|
||||||
|
getToolName,
|
||||||
|
isToolStateCompleted,
|
||||||
|
isToolStateError,
|
||||||
|
isToolStateRunning,
|
||||||
|
getDefaultToolAction,
|
||||||
|
readToolStatePayload,
|
||||||
|
} from "./tool-call/utils"
|
||||||
import { resolveTitleForTool } from "./tool-call/tool-title"
|
import { resolveTitleForTool } from "./tool-call/tool-title"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
|
||||||
@@ -155,12 +166,33 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const prefExpanded = toolOutputDefaultExpanded()
|
const prefExpanded = toolOutputDefaultExpanded()
|
||||||
const toolName = toolCallMemo()?.tool || ""
|
const toolName = toolCallMemo()?.tool || ""
|
||||||
if (toolName === "read") {
|
if (toolName === "read") {
|
||||||
|
const state = toolState()
|
||||||
|
if (state?.status === "error") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return prefExpanded
|
return prefExpanded
|
||||||
})
|
})
|
||||||
|
|
||||||
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
|
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
|
||||||
|
const toolInputsVisibility = createMemo(() => preferences().toolInputsVisibility || "collapsed")
|
||||||
|
const [toolInputVisibilityOverride, setToolInputVisibilityOverride] = createSignal<"hidden" | "expanded" | null>(null)
|
||||||
|
const effectiveToolInputsVisibility = createMemo(() => toolInputVisibilityOverride() ?? toolInputsVisibility())
|
||||||
|
const isToolInputVisible = createMemo(() => effectiveToolInputsVisibility() !== "hidden")
|
||||||
|
const inputDefaultExpanded = createMemo(() => effectiveToolInputsVisibility() === "expanded")
|
||||||
|
const [inputSectionOverride, setInputSectionOverride] = createSignal<boolean | null>(null)
|
||||||
|
const [outputSectionOverride, setOutputSectionOverride] = createSignal<boolean | null>(null)
|
||||||
|
const inputSectionExpanded = () => {
|
||||||
|
const override = inputSectionOverride()
|
||||||
|
if (override !== null) return override
|
||||||
|
return inputDefaultExpanded()
|
||||||
|
}
|
||||||
|
const outputSectionExpanded = () => {
|
||||||
|
const override = outputSectionOverride()
|
||||||
|
if (override !== null) return override
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const isPermissionActive = createMemo(() => {
|
const isPermissionActive = createMemo(() => {
|
||||||
const pending = pendingPermission()
|
const pending = pendingPermission()
|
||||||
@@ -183,6 +215,35 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
return defaultExpandedForTool()
|
return defaultExpandedForTool()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolInput = createMemo(() => {
|
||||||
|
const state = toolState()
|
||||||
|
return readToolStatePayload(state).input
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasToolInput = createMemo(() => {
|
||||||
|
const input = toolInput()
|
||||||
|
return input && Object.keys(input).length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolInputMarkdown = createMemo(() => {
|
||||||
|
const input = toolInput()
|
||||||
|
if (!input || Object.keys(input).length === 0) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const yamlText = stringifyYaml(input)
|
||||||
|
return ensureMarkdownContent(yamlText, "yaml", true)
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to convert tool call input to YAML", error)
|
||||||
|
try {
|
||||||
|
const jsonText = JSON.stringify(input, null, 2)
|
||||||
|
return ensureMarkdownContent(jsonText, "json", true)
|
||||||
|
} catch (nestedError) {
|
||||||
|
log.error("Failed to stringify tool call input", nestedError)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const permissionDetails = createMemo(() => pendingPermission()?.permission)
|
const permissionDetails = createMemo(() => pendingPermission()?.permission)
|
||||||
const questionDetails = createMemo(() => pendingQuestion()?.request)
|
const questionDetails = createMemo(() => pendingQuestion()?.request)
|
||||||
|
|
||||||
@@ -515,13 +576,13 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
const status = toolState()?.status || ""
|
const status = toolState()?.status || ""
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "pending":
|
case "pending":
|
||||||
return "⏸"
|
|
||||||
case "running":
|
|
||||||
return "⏳"
|
return "⏳"
|
||||||
|
case "running":
|
||||||
|
return "🔄"
|
||||||
case "completed":
|
case "completed":
|
||||||
return "✓"
|
return "✅"
|
||||||
case "error":
|
case "error":
|
||||||
return "✗"
|
return "⚠️"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -548,6 +609,25 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// When global preference changes, reset per-tool-call overrides so palette changes apply.
|
||||||
|
toolInputsVisibility()
|
||||||
|
setToolInputVisibilityOverride(null)
|
||||||
|
setInputSectionOverride(null)
|
||||||
|
setOutputSectionOverride(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleToggleInputVisibility = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!expanded()) {
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentlyVisible = isToolInputVisible()
|
||||||
|
setToolInputVisibilityOverride(currentlyVisible ? "hidden" : "expanded")
|
||||||
|
}
|
||||||
|
|
||||||
const renderer = createMemo(() => resolveToolRenderer(toolName()))
|
const renderer = createMemo(() => resolveToolRenderer(toolName()))
|
||||||
|
|
||||||
const { renderAnsiContent } = createAnsiContentRenderer({
|
const { renderAnsiContent } = createAnsiContentRenderer({
|
||||||
@@ -789,6 +869,23 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Show when={hasToolInput()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-header-input"
|
||||||
|
onClick={handleToggleInputVisibility}
|
||||||
|
aria-pressed={isToolInputVisible()}
|
||||||
|
aria-label={
|
||||||
|
isToolInputVisible()
|
||||||
|
? t("toolCall.header.hideInputAriaLabel")
|
||||||
|
: t("toolCall.header.showInputAriaLabel")
|
||||||
|
}
|
||||||
|
title={isToolInputVisible() ? t("toolCall.header.hideInputTitle") : t("toolCall.header.showInputTitle")}
|
||||||
|
>
|
||||||
|
<ArrowRightSquare class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tool-call-header-copy"
|
class="tool-call-header-copy"
|
||||||
@@ -806,19 +903,79 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
|
|
||||||
{expanded() && (
|
{expanded() && (
|
||||||
<div class="tool-call-details">
|
<div class="tool-call-details">
|
||||||
{renderToolBody()}
|
<Show
|
||||||
|
when={isToolInputVisible() && hasToolInput()}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
|
{renderToolBody()}
|
||||||
|
{renderError()}
|
||||||
|
|
||||||
{renderError()}
|
<Show when={status() === "pending" && !pendingPermission()}>
|
||||||
|
<div class="tool-call-pending-message">
|
||||||
|
<span class="spinner-small"></span>
|
||||||
|
<span>{t("toolCall.pending.waitingToRun")}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="tool-call-io-sections">
|
||||||
|
<div class="tool-call-io-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-io-toggle"
|
||||||
|
aria-expanded={inputSectionExpanded()}
|
||||||
|
onClick={() => setInputSectionOverride((prev) => {
|
||||||
|
const current = prev === null ? inputSectionExpanded() : prev
|
||||||
|
return !current
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span class="tool-call-io-title">{t("toolCall.io.input")}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={inputSectionExpanded()}>
|
||||||
|
<div class="tool-call-io-body">
|
||||||
|
{(() => {
|
||||||
|
const content = toolInputMarkdown()
|
||||||
|
if (!content) return null
|
||||||
|
return renderMarkdownContent({ content, cacheKey: "input" })
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-call-io-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-call-io-toggle"
|
||||||
|
aria-expanded={outputSectionExpanded()}
|
||||||
|
onClick={() => setOutputSectionOverride((prev) => {
|
||||||
|
const current = prev === null ? outputSectionExpanded() : prev
|
||||||
|
return !current
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span class="tool-call-io-title">{t("toolCall.io.output")}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={outputSectionExpanded()}>
|
||||||
|
<div class="tool-call-io-body">
|
||||||
|
{renderToolBody()}
|
||||||
|
{renderError()}
|
||||||
|
|
||||||
|
<Show when={status() === "pending" && !pendingPermission()}>
|
||||||
|
<div class="tool-call-pending-message">
|
||||||
|
<span class="spinner-small"></span>
|
||||||
|
<span>{t("toolCall.pending.waitingToRun")}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{renderPermissionBlock()}
|
{renderPermissionBlock()}
|
||||||
{renderQuestionBlock()}
|
{renderQuestionBlock()}
|
||||||
|
|
||||||
<Show when={status() === "pending" && !pendingPermission()}>
|
|
||||||
<div class="tool-call-pending-message">
|
|
||||||
<span class="spinner-small"></span>
|
|
||||||
<span>{t("toolCall.pending.waitingToRun")}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSignal, onMount } from "solid-js"
|
import { createSignal, onMount } from "solid-js"
|
||||||
import type { Accessor } from "solid-js"
|
import type { Accessor } from "solid-js"
|
||||||
import type { Preferences, ExpansionPreference } from "../../stores/preferences"
|
import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference } from "../../stores/preferences"
|
||||||
import { createCommandRegistry, type Command } from "../commands"
|
import { createCommandRegistry, type Command } from "../commands"
|
||||||
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
||||||
import type { ClientPart, MessageInfo } from "../../types/message"
|
import type { ClientPart, MessageInfo } from "../../types/message"
|
||||||
@@ -38,6 +38,7 @@ export interface UseCommandsOptions {
|
|||||||
setToolOutputExpansion: (mode: ExpansionPreference) => void
|
setToolOutputExpansion: (mode: ExpansionPreference) => void
|
||||||
setDiagnosticsExpansion: (mode: ExpansionPreference) => void
|
setDiagnosticsExpansion: (mode: ExpansionPreference) => void
|
||||||
setThinkingBlocksExpansion: (mode: ExpansionPreference) => void
|
setThinkingBlocksExpansion: (mode: ExpansionPreference) => void
|
||||||
|
setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void
|
||||||
handleNewInstanceRequest: () => void
|
handleNewInstanceRequest: () => void
|
||||||
handleCloseInstance: (instanceId: string) => Promise<void>
|
handleCloseInstance: (instanceId: string) => Promise<void>
|
||||||
handleNewSession: (instanceId: string) => Promise<void>
|
handleNewSession: (instanceId: string) => Promise<void>
|
||||||
@@ -551,6 +552,29 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
commandRegistry.register({
|
||||||
|
id: "tool-inputs-visibility",
|
||||||
|
label: () => {
|
||||||
|
const mode = options.preferences().toolInputsVisibility || "hidden"
|
||||||
|
const state =
|
||||||
|
mode === "expanded"
|
||||||
|
? tGlobal("commands.common.expanded")
|
||||||
|
: mode === "collapsed"
|
||||||
|
? tGlobal("commands.common.collapsed")
|
||||||
|
: tGlobal("commands.common.hidden")
|
||||||
|
return tGlobal("commands.toolInputsVisibility.label", { state })
|
||||||
|
},
|
||||||
|
description: () => tGlobal("commands.toolInputsVisibility.description"),
|
||||||
|
category: "System",
|
||||||
|
keywords: () => splitKeywords("commands.toolInputsVisibility.keywords"),
|
||||||
|
action: () => {
|
||||||
|
const mode = options.preferences().toolInputsVisibility || "hidden"
|
||||||
|
const next: ToolInputsVisibilityPreference =
|
||||||
|
mode === "hidden" ? "collapsed" : mode === "collapsed" ? "expanded" : "hidden"
|
||||||
|
options.setToolInputsVisibility(next)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
commandRegistry.register({
|
commandRegistry.register({
|
||||||
id: "token-usage-visibility",
|
id: "token-usage-visibility",
|
||||||
label: () => {
|
label: () => {
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output",
|
"commands.diagnosticsDefault.description": "Toggle default expansion for diagnostics output",
|
||||||
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse",
|
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "Tool Inputs Visibility · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "Set default visibility for tool call input arguments",
|
||||||
|
"commands.toolInputsVisibility.keywords": "tool, inputs, arguments, visibility, hide, show, expand, collapse",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "Token Usage Display · {state}",
|
"commands.tokenUsageDisplay.label": "Token Usage Display · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "Show or hide token and cost stats for assistant messages",
|
"commands.tokenUsageDisplay.description": "Show or hide token and cost stats for assistant messages",
|
||||||
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats",
|
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "Diff view mode",
|
"toolCall.diff.viewMode.ariaLabel": "Diff view mode",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos",
|
"commands.diagnosticsDefault.description": "Alternar la expansión por defecto de la salida de diagnósticos",
|
||||||
"commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar",
|
"commands.diagnosticsDefault.keywords": "diagnósticos, expandir, colapsar",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "Visibilidad de entradas de herramientas · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "Configurar la visibilidad por defecto de los argumentos de entrada de llamadas de herramienta",
|
||||||
|
"commands.toolInputsVisibility.keywords": "herramienta, entradas, argumentos, visibilidad, ocultar, mostrar, expandir, colapsar",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "Mostrar uso de tokens · {state}",
|
"commands.tokenUsageDisplay.label": "Mostrar uso de tokens · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "Mostrar u ocultar estadísticas de tokens y costo en los mensajes del asistente",
|
"commands.tokenUsageDisplay.description": "Mostrar u ocultar estadísticas de tokens y costo en los mensajes del asistente",
|
||||||
"commands.tokenUsageDisplay.keywords": "token, uso, costo, estadísticas",
|
"commands.tokenUsageDisplay.keywords": "token, uso, costo, estadísticas",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",
|
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics",
|
"commands.diagnosticsDefault.description": "Choisir l'ouverture par défaut de la sortie des diagnostics",
|
||||||
"commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire",
|
"commands.diagnosticsDefault.keywords": "diagnostics, développer, réduire",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "Visibilité des entrées d'outil · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "Définir la visibilité par défaut des arguments d'entrée des appels d'outil",
|
||||||
|
"commands.toolInputsVisibility.keywords": "outil, entrées, arguments, visibilité, masquer, afficher, développer, réduire",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "Affichage de l'usage des tokens · {state}",
|
"commands.tokenUsageDisplay.label": "Affichage de l'usage des tokens · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "Afficher ou masquer les stats de tokens et de coût pour les messages de l'assistant",
|
"commands.tokenUsageDisplay.description": "Afficher ou masquer les stats de tokens et de coût pour les messages de l'assistant",
|
||||||
"commands.tokenUsageDisplay.keywords": "token, usage, coût, stats",
|
"commands.tokenUsageDisplay.keywords": "token, usage, coût, stats",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff",
|
"toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "診断出力を既定で展開するか切り替え",
|
"commands.diagnosticsDefault.description": "診断出力を既定で展開するか切り替え",
|
||||||
"commands.diagnosticsDefault.keywords": "診断, 展開, 折りたたみ, diagnostics, expand, collapse",
|
"commands.diagnosticsDefault.keywords": "診断, 展開, 折りたたみ, diagnostics, expand, collapse",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "ツール入力の表示 · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "ツール呼び出しの入力引数の既定の表示状態を設定します",
|
||||||
|
"commands.toolInputsVisibility.keywords": "ツール, 入力, 引数, 表示, 非表示, 展開, 折りたたみ, tool, inputs, arguments, visibility, hide, show, expand, collapse",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "トークン使用量表示 · {state}",
|
"commands.tokenUsageDisplay.label": "トークン使用量表示 · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "アシスタントメッセージのトークン/コスト統計を表示/非表示",
|
"commands.tokenUsageDisplay.description": "アシスタントメッセージのトークン/コスト統計を表示/非表示",
|
||||||
"commands.tokenUsageDisplay.keywords": "トークン, 使用量, コスト, 統計, token, usage, cost, stats",
|
"commands.tokenUsageDisplay.keywords": "トークン, 使用量, コスト, 統計, token, usage, cost, stats",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード",
|
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "Переключить, разворачивать ли вывод диагностики по умолчанию",
|
"commands.diagnosticsDefault.description": "Переключить, разворачивать ли вывод диагностики по умолчанию",
|
||||||
"commands.diagnosticsDefault.keywords": "diagnostics, развернуть, свернуть",
|
"commands.diagnosticsDefault.keywords": "diagnostics, развернуть, свернуть",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "Видимость входных данных инструмента · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "Установить видимость аргументов входа вызовов инструментов по умолчанию",
|
||||||
|
"commands.toolInputsVisibility.keywords": "инструмент, вход, аргументы, видимость, скрыть, показать, раскрыть, свернуть, tool, inputs, arguments, visibility, hide, show, expand, collapse",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "Отображение token-статистики · {state}",
|
"commands.tokenUsageDisplay.label": "Отображение token-статистики · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "Показать или скрыть статистику token и стоимости для сообщений ассистента",
|
"commands.tokenUsageDisplay.description": "Показать или скрыть статистику token и стоимости для сообщений ассистента",
|
||||||
"commands.tokenUsageDisplay.keywords": "token, usage, cost, статистика",
|
"commands.tokenUsageDisplay.keywords": "token, usage, cost, статистика",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff",
|
"toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ export const commandMessages = {
|
|||||||
"commands.diagnosticsDefault.description": "切换诊断输出是否默认展开",
|
"commands.diagnosticsDefault.description": "切换诊断输出是否默认展开",
|
||||||
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse, 诊断, 展开, 折叠",
|
"commands.diagnosticsDefault.keywords": "diagnostics, expand, collapse, 诊断, 展开, 折叠",
|
||||||
|
|
||||||
|
"commands.toolInputsVisibility.label": "工具输入可见性 · {state}",
|
||||||
|
"commands.toolInputsVisibility.description": "设置工具调用输入参数的默认可见性",
|
||||||
|
"commands.toolInputsVisibility.keywords": "工具, 输入, 参数, 可见性, 隐藏, 显示, 展开, 折叠, tool, inputs, arguments, visibility, hide, show, expand, collapse",
|
||||||
|
|
||||||
"commands.tokenUsageDisplay.label": "Token 使用显示 · {state}",
|
"commands.tokenUsageDisplay.label": "Token 使用显示 · {state}",
|
||||||
"commands.tokenUsageDisplay.description": "显示或隐藏助手消息的 token 和费用统计",
|
"commands.tokenUsageDisplay.description": "显示或隐藏助手消息的 token 和费用统计",
|
||||||
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats, 令牌, 用量, 费用, 统计",
|
"commands.tokenUsageDisplay.keywords": "token, usage, cost, stats, 令牌, 用量, 费用, 统计",
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export const toolCallMessages = {
|
|||||||
"toolCall.header.copyTitle": "Copy tool call title",
|
"toolCall.header.copyTitle": "Copy tool call title",
|
||||||
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
"toolCall.header.copyAriaLabel": "Copy tool call title",
|
||||||
|
|
||||||
|
"toolCall.header.showInputTitle": "Show Tool Arguments",
|
||||||
|
"toolCall.header.showInputAriaLabel": "Show Tool Arguments",
|
||||||
|
"toolCall.header.hideInputTitle": "Hide Tool Arguments",
|
||||||
|
"toolCall.header.hideInputAriaLabel": "Hide Tool Arguments",
|
||||||
|
|
||||||
|
"toolCall.io.input": "Tool Input",
|
||||||
|
"toolCall.io.output": "Tool Output",
|
||||||
|
|
||||||
"toolCall.diff.label": "Diff",
|
"toolCall.diff.label": "Diff",
|
||||||
"toolCall.diff.label.withPath": "Diff · {path}",
|
"toolCall.diff.label.withPath": "Diff · {path}",
|
||||||
"toolCall.diff.viewMode.ariaLabel": "Diff 视图模式",
|
"toolCall.diff.viewMode.ariaLabel": "Diff 视图模式",
|
||||||
|
|||||||
29
packages/ui/src/lib/launch-errors.ts
Normal file
29
packages/ui/src/lib/launch-errors.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export function formatLaunchErrorMessage(error: unknown, fallbackMessage: string): string {
|
||||||
|
if (!error) {
|
||||||
|
return fallbackMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (parsed && typeof parsed === "object" && "error" in parsed && typeof (parsed as any).error === "string") {
|
||||||
|
return (parsed as any).error
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore JSON parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMissingBinaryMessage(message: string): boolean {
|
||||||
|
const normalized = message.toLowerCase()
|
||||||
|
return (
|
||||||
|
normalized.includes("opencode binary not found") ||
|
||||||
|
normalized.includes("binary not found") ||
|
||||||
|
normalized.includes("no such file or directory") ||
|
||||||
|
normalized.includes("binary is not executable") ||
|
||||||
|
normalized.includes("enoent")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import { upsertPermissionV2, removePermissionV2, upsertQuestionV2, removeQuestio
|
|||||||
import { clearCacheForInstance } from "../lib/global-cache"
|
import { clearCacheForInstance } from "../lib/global-cache"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
|
import { mergeInstanceMetadata, clearInstanceMetadata } from "./instance-metadata"
|
||||||
|
import { showWorkspaceLaunchError } from "./launch-errors"
|
||||||
|
|
||||||
const log = getLogger("api")
|
const log = getLogger("api")
|
||||||
|
|
||||||
@@ -372,6 +373,7 @@ function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
|||||||
break
|
break
|
||||||
case "workspace.error":
|
case "workspace.error":
|
||||||
upsertWorkspace(event.workspace)
|
upsertWorkspace(event.workspace)
|
||||||
|
showWorkspaceLaunchError(event.workspace)
|
||||||
break
|
break
|
||||||
case "workspace.stopped":
|
case "workspace.stopped":
|
||||||
releaseInstanceResources(event.workspaceId)
|
releaseInstanceResources(event.workspaceId)
|
||||||
|
|||||||
53
packages/ui/src/stores/launch-errors.ts
Normal file
53
packages/ui/src/stores/launch-errors.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
import type { WorkspaceDescriptor } from "../../../server/src/api-types"
|
||||||
|
import { tGlobal } from "../lib/i18n"
|
||||||
|
import { formatLaunchErrorMessage, isMissingBinaryMessage } from "../lib/launch-errors"
|
||||||
|
|
||||||
|
type LaunchErrorSource = "create" | "workspace"
|
||||||
|
|
||||||
|
export interface LaunchErrorState {
|
||||||
|
source: LaunchErrorSource
|
||||||
|
message: string
|
||||||
|
binaryPath: string
|
||||||
|
missingBinary: boolean
|
||||||
|
instanceId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
||||||
|
|
||||||
|
// Avoid spamming the user with the same modal on repeated events.
|
||||||
|
const lastWorkspaceErrorByInstanceId = new Map<string, string>()
|
||||||
|
|
||||||
|
export function showLaunchError(next: LaunchErrorState) {
|
||||||
|
setLaunchError(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLaunchError() {
|
||||||
|
setLaunchError(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showWorkspaceLaunchError(workspace: WorkspaceDescriptor) {
|
||||||
|
const instanceId = workspace.id
|
||||||
|
const rawMessage = workspace.error
|
||||||
|
const message = formatLaunchErrorMessage(rawMessage, tGlobal("app.launchError.fallbackMessage"))
|
||||||
|
|
||||||
|
const previous = lastWorkspaceErrorByInstanceId.get(instanceId)
|
||||||
|
if (previous && previous === message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastWorkspaceErrorByInstanceId.set(instanceId, message)
|
||||||
|
|
||||||
|
const binaryPath = (workspace.binaryLabel || workspace.binaryId || "opencode").trim() || "opencode"
|
||||||
|
const missingBinary = isMissingBinaryMessage(message)
|
||||||
|
|
||||||
|
showLaunchError({
|
||||||
|
source: "workspace",
|
||||||
|
instanceId,
|
||||||
|
message,
|
||||||
|
binaryPath,
|
||||||
|
missingBinary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { launchError }
|
||||||
@@ -25,6 +25,7 @@ export interface ModelPreference {
|
|||||||
|
|
||||||
export type DiffViewMode = "split" | "unified"
|
export type DiffViewMode = "split" | "unified"
|
||||||
export type ExpansionPreference = "expanded" | "collapsed"
|
export type ExpansionPreference = "expanded" | "collapsed"
|
||||||
|
export type ToolInputsVisibilityPreference = "hidden" | "collapsed" | "expanded"
|
||||||
export type ListeningMode = "local" | "all"
|
export type ListeningMode = "local" | "all"
|
||||||
|
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
@@ -37,6 +38,7 @@ export interface UiSettings {
|
|||||||
diffViewMode: DiffViewMode
|
diffViewMode: DiffViewMode
|
||||||
toolOutputExpansion: ExpansionPreference
|
toolOutputExpansion: ExpansionPreference
|
||||||
diagnosticsExpansion: ExpansionPreference
|
diagnosticsExpansion: ExpansionPreference
|
||||||
|
toolInputsVisibility: ToolInputsVisibilityPreference
|
||||||
showUsageMetrics: boolean
|
showUsageMetrics: boolean
|
||||||
autoCleanupBlankSessions: boolean
|
autoCleanupBlankSessions: boolean
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ const defaultUiSettings: UiSettings = {
|
|||||||
diffViewMode: "split",
|
diffViewMode: "split",
|
||||||
toolOutputExpansion: "expanded",
|
toolOutputExpansion: "expanded",
|
||||||
diagnosticsExpansion: "expanded",
|
diagnosticsExpansion: "expanded",
|
||||||
|
toolInputsVisibility: "collapsed",
|
||||||
showUsageMetrics: true,
|
showUsageMetrics: true,
|
||||||
autoCleanupBlankSessions: true,
|
autoCleanupBlankSessions: true,
|
||||||
|
|
||||||
@@ -130,6 +133,10 @@ function normalizeUiSettings(input?: Partial<UiSettings> | null): UiSettings {
|
|||||||
diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode,
|
diffViewMode: sanitized.diffViewMode ?? defaultUiSettings.diffViewMode,
|
||||||
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion,
|
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultUiSettings.toolOutputExpansion,
|
||||||
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion,
|
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultUiSettings.diagnosticsExpansion,
|
||||||
|
toolInputsVisibility:
|
||||||
|
sanitized.toolInputsVisibility === "hidden" || sanitized.toolInputsVisibility === "collapsed" || sanitized.toolInputsVisibility === "expanded"
|
||||||
|
? sanitized.toolInputsVisibility
|
||||||
|
: defaultUiSettings.toolInputsVisibility,
|
||||||
showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics,
|
showUsageMetrics: sanitized.showUsageMetrics ?? defaultUiSettings.showUsageMetrics,
|
||||||
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions,
|
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultUiSettings.autoCleanupBlankSessions,
|
||||||
osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled,
|
osNotificationsEnabled: sanitized.osNotificationsEnabled ?? defaultUiSettings.osNotificationsEnabled,
|
||||||
@@ -439,6 +446,11 @@ function setDiagnosticsExpansion(mode: ExpansionPreference): void {
|
|||||||
updateUiSettings({ diagnosticsExpansion: mode })
|
updateUiSettings({ diagnosticsExpansion: mode })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setToolInputsVisibility(mode: ToolInputsVisibilityPreference): void {
|
||||||
|
if (preferences().toolInputsVisibility === mode) return
|
||||||
|
updateUiSettings({ toolInputsVisibility: mode })
|
||||||
|
}
|
||||||
|
|
||||||
function setThinkingBlocksExpansion(mode: ExpansionPreference): void {
|
function setThinkingBlocksExpansion(mode: ExpansionPreference): void {
|
||||||
if (preferences().thinkingBlocksExpansion === mode) return
|
if (preferences().thinkingBlocksExpansion === mode) return
|
||||||
updateUiSettings({ thinkingBlocksExpansion: mode })
|
updateUiSettings({ thinkingBlocksExpansion: mode })
|
||||||
@@ -536,6 +548,7 @@ interface ConfigContextValue {
|
|||||||
setToolOutputExpansion: typeof setToolOutputExpansion
|
setToolOutputExpansion: typeof setToolOutputExpansion
|
||||||
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
||||||
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
||||||
|
setToolInputsVisibility: typeof setToolInputsVisibility
|
||||||
|
|
||||||
// instance scoped
|
// instance scoped
|
||||||
setAgentModelPreference: typeof setAgentModelPreference
|
setAgentModelPreference: typeof setAgentModelPreference
|
||||||
@@ -579,6 +592,7 @@ const configContextValue: ConfigContextValue = {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setToolInputsVisibility,
|
||||||
setAgentModelPreference,
|
setAgentModelPreference,
|
||||||
getAgentModelPreference,
|
getAgentModelPreference,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
@apply flex items-stretch w-full;
|
@apply flex items-stretch w-full;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--tool-call-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-header:hover {
|
.tool-call-header:hover {
|
||||||
@@ -127,11 +128,30 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-call-header-input {
|
||||||
|
@apply inline-flex items-center justify-center;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-header-copy:hover {
|
.tool-call-header-copy:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-call-header-input:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-header-input[aria-pressed="true"] {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-header-status {
|
.tool-call-header-status {
|
||||||
@apply inline-flex items-center justify-center;
|
@apply inline-flex items-center justify-center;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
@@ -213,6 +233,63 @@
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tool-call-io-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-section {
|
||||||
|
border: 1px solid var(--tool-call-border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: var(--surface-secondary);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--tool-call-border-color);
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-toggle::before {
|
||||||
|
content: "▶";
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 0.35rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-toggle[aria-expanded="true"]::before {
|
||||||
|
content: "▼";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-title {
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-io-body {
|
||||||
|
background-color: var(--surface-code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IO sections provide the outer frame; avoid double borders on markdown frames. */
|
||||||
|
.tool-call-io-body .tool-call-markdown {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-markdown {
|
.tool-call-markdown {
|
||||||
background-color: var(--surface-code);
|
background-color: var(--surface-code);
|
||||||
/* Keep a visible frame around the scroll viewport (not the content). */
|
/* Keep a visible frame around the scroll viewport (not the content). */
|
||||||
|
|||||||
Reference in New Issue
Block a user