diff --git a/web/src/components/AssistantChat/HappyComposer.tsx b/web/src/components/AssistantChat/HappyComposer.tsx
index afdea1a4..5603c3f8 100644
--- a/web/src/components/AssistantChat/HappyComposer.tsx
+++ b/web/src/components/AssistantChat/HappyComposer.tsx
@@ -269,85 +269,93 @@ export function HappyComposer(props: {
haptic('light')
}, [onModelModeChange, controlsDisabled, haptic])
+ const showPermissionSettings = Boolean(onPermissionModeChange)
+ const showModelSettings = Boolean(onModelModeChange)
const showSettingsButton = Boolean(onPermissionModeChange || onModelModeChange)
const showAbortButton = true
const overlays = useMemo(() => {
- if (showSettings) {
+ if (showSettings && (showPermissionSettings || showModelSettings)) {
return (
-
-
- Permission Mode
-
- {PERMISSION_MODES.map((mode) => (
-
- ))}
-
-
-
-
-
-
- Model
+
+ {permissionMode === mode && (
+
+ )}
+
+
+ {PERMISSION_MODE_LABELS[mode]}
+
+
+ ))}
- {MODEL_MODES.map((mode) => (
-
handleModelChange(mode)}
- onMouseDown={(e) => e.preventDefault()}
- >
-
+ ) : null}
+
+ {showModelSettings ? (
+
+
+ Model
+
+ {MODEL_MODES.map((mode) => (
+
handleModelChange(mode)}
+ onMouseDown={(e) => e.preventDefault()}
>
- {modelMode === mode && (
-
- )}
-
-
- {MODEL_MODE_LABELS[mode]}
-
-
- ))}
-
+
+ {modelMode === mode && (
+
+ )}
+
+
+ {MODEL_MODE_LABELS[mode]}
+
+
+ ))}
+
+ ) : null}
)
@@ -370,6 +378,8 @@ export function HappyComposer(props: {
return null
}, [
showSettings,
+ showPermissionSettings,
+ showModelSettings,
suggestions,
selectedIndex,
controlsDisabled,
diff --git a/web/src/components/AssistantChat/messages/ToolMessage.tsx b/web/src/components/AssistantChat/messages/ToolMessage.tsx
index f004fae0..24b3b11d 100644
--- a/web/src/components/AssistantChat/messages/ToolMessage.tsx
+++ b/web/src/components/AssistantChat/messages/ToolMessage.tsx
@@ -2,6 +2,7 @@ import type { ToolCallMessagePartProps } from '@assistant-ui/react'
import type { ChatBlock } from '@/chat/types'
import type { AgentEvent, ToolCallBlock } from '@/chat/types'
import type { MessageStatus } from '@/types/api'
+import { CodeBlock } from '@/components/CodeBlock'
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
import { LazyRainbowText } from '@/components/LazyRainbowText'
import { ToolCard } from '@/components/ToolCard/ToolCard'
@@ -11,11 +12,28 @@ function isObject(value: unknown): value is Record {
return Boolean(value) && typeof value === 'object'
}
+function safeStringify(value: unknown): string {
+ if (typeof value === 'string') return value
+ try {
+ const stringified = JSON.stringify(value, null, 2)
+ return typeof stringified === 'string' ? stringified : String(value)
+ } catch {
+ return String(value)
+ }
+}
+
function isToolCallBlock(value: unknown): value is ToolCallBlock {
if (!isObject(value)) return false
if (value.kind !== 'tool-call') return false
if (typeof value.id !== 'string') return false
+ if (value.localId !== null && typeof value.localId !== 'string') return false
+ if (typeof value.createdAt !== 'number') return false
+ if (!Array.isArray(value.children)) return false
if (!isObject(value.tool)) return false
+ if (typeof value.tool.name !== 'string') return false
+ if (!('input' in value.tool)) return false
+ if (value.tool.description !== null && typeof value.tool.description !== 'string') return false
+ if (value.tool.state !== 'pending' && value.tool.state !== 'running' && value.tool.state !== 'completed' && value.tool.state !== 'error') return false
return true
}
@@ -182,10 +200,37 @@ export function HappyToolMessage(props: ToolCallMessagePartProps) {
const artifact = props.artifact
if (!isToolCallBlock(artifact)) {
+ const argsText = typeof props.argsText === 'string' ? props.argsText.trim() : ''
+ const hasArgsText = argsText.length > 0
+ const hasResult = props.result !== undefined
+ const resultText = hasResult ? safeStringify(props.result) : ''
+
return (
-
- Tool call: {props.toolName}
+
+
+
+ Tool: {props.toolName}
+
+ {props.isError ? (
+
Error
+ ) : null}
+ {props.status.type === 'running' && !hasResult ? (
+
Running…
+ ) : null}
+
+
+ {hasArgsText ? (
+
+
+
+ ) : null}
+
+ {hasResult ? (
+
+
+
+ ) : null}
)