@@ -61,6 +65,22 @@
import ThinkingCard from './ThinkingCard.vue'
import { renderMarkdown } from '@/composables/useMarkdown.js'
+function looksLikeMarkdown(str) {
+ if (typeof str !== 'string') return false
+ const mdPatterns = [
+ /#{1,6} /, // headings
+ /\*\*|__/, // bold
+ /`[^`]+`/, // inline code
+ /```/, // code block
+ /^\s*[-*+]\s/m, // list
+ /^\s*\d+\.\s/m, // ordered list
+ /\[[^\]]+\]\([^)]+\)/, // link
+ /\|.*\|/, // table
+ /^\s*>\s/m, // blockquote
+ ]
+ return mdPatterns.some(re => re.test(str))
+}
+
const props = defineProps({ tool: { type: Object, required: true } })
const detailsEl = ref(null)
@@ -93,20 +113,6 @@
onBeforeUnmount(stopTimer)
-const TOOL_ICONS = {
- web_search: '🔍',
- web_fetch: '🌐',
- filesystem: '📁',
- read_file: '📄',
- write_file: '📝',
- code_exec: '⚙️',
- terminal: '💻',
- ssh_exec: '🖥️',
- spawn_agent: '🤖',
- switch_profile: '🔄',
-}
-
-const toolIcon = computed(() => TOOL_ICONS[props.tool.name] ?? '🔧')
const runningLabel = computed(() => {
if (props.tool.name === 'filesystem' && props.tool.args?.action) {
return `Running filesystem ${props.tool.args.action}...`
@@ -131,15 +137,77 @@
timer = null
}
-function formatJson(val) {
- if (typeof val === 'string') {
- try { return JSON.stringify(JSON.parse(val), null, 2) } catch { return val }
- }
- return JSON.stringify(val, null, 2)
+function renderArgs(val) {
+ return formatCompactJson(val)
}
-function formatResult(val) {
- if (typeof val === 'string') return val
- return JSON.stringify(val, null, 2)
+function renderResult(val) {
+ if (typeof val === 'string') {
+ // Try JSON first
+ try {
+ const parsed = JSON.parse(val)
+ return formatCompactJson(parsed)
+ } catch { /* not JSON */ }
+
+ if (looksLikeMarkdown(val)) {
+ return renderMarkdown(val)
+ }
+
+ return escapeHtml(val)
+ .split('\n')
+ .map((line, i) => `
${line || ' '}
`)
+ .join('')
+ }
+ return formatCompactJson(val)
+}
+
+function formatCompactJson(val) {
+ if (typeof val === 'string') {
+ try { val = JSON.parse(val) } catch { return escapeHtml(val) }
+ }
+ if (val === null || typeof val !== 'object') return escapeHtml(String(val))
+ if (Array.isArray(val)) {
+ if (val.length === 0) return '
[]'
+ const items = val.map((item, i) =>
+ `
+ ${i}
+ :
+ ${formatCompactValue(item)}
+
`
+ ).join('')
+ return `
${items}
`
+ }
+ const entries = Object.entries(val)
+ if (entries.length === 0) return '
{}'
+ const items = entries.map(([k, v]) =>
+ `
+ ${escapeHtml(k)}
+ :
+ ${formatCompactValue(v)}
+
`
+ ).join('')
+ return `
${items}
`
+}
+
+function formatCompactValue(v) {
+ if (v === null) return '
null'
+ if (typeof v === 'boolean') return `
${v}`
+ if (typeof v === 'number') return `
${v}`
+ if (typeof v === 'string') {
+ const max = 200
+ const s = escapeHtml(v)
+ if (s.length <= max) return `
${s}`
+ return `
${s.slice(0, max)}…`
+ }
+ if (Array.isArray(v)) return `
[${v.length} items]`
+ return `
{${Object.keys(v).length} keys}`
+}
+
+function escapeHtml(str) {
+ if (typeof str !== 'string') return String(str ?? '')
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
}
diff --git a/webclient/src/styles/app.scss b/webclient/src/styles/app.scss
index ac0fbc8..2cc96d5 100644
--- a/webclient/src/styles/app.scss
+++ b/webclient/src/styles/app.scss
@@ -802,17 +802,15 @@
// ─── Thinking card ─────────────────────────────────────────────────────────
.thinking-card {
- margin-bottom: $space-sm;
-
summary {
list-style: none;
display: flex;
align-items: center;
gap: $space-sm;
- padding: $space-sm $space-md;
+ padding: $space-xs $space-md;
cursor: pointer;
color: $color-text-dark;
- font-size: 13px;
+ font-size: 12px;
font-style: italic;
background: $surface-panel-muted;
border: $border-width-base $border-style-base $border-color-muted;
@@ -840,46 +838,47 @@
}
.thinking-body {
- padding: $space-md;
- font-size: 13px;
+ padding: $space-sm $space-md;
+ font-size: 12px;
font-style: italic;
color: $color-text-medium;
- line-height: 1.6;
+ line-height: 1.5;
background: $surface-panel-muted;
border: $border-width-base $border-style-base $border-color-muted;
border-top: none;
white-space: pre-wrap;
+ word-break: break-word;
}
}
// ─── Plan card ─────────────────────────────────────────────────────────────
.plan-card {
- border: $border-width-base $border-style-base $color-info;
+ border: $border-width-base $border-style-base $color-secondary;
border-left-width: $border-width-accent;
- border-left-color: $color-info;
+ border-left-color: $color-secondary;
overflow: hidden;
- font-size: 13px;
- margin-bottom: $space-sm;
+ font-size: 12px;
+ margin-bottom: 8px;
summary {
list-style: none;
display: flex;
align-items: center;
gap: $space-sm;
- padding: $space-sm $space-md;
+ padding: $space-xs $space-md;
cursor: pointer;
- background: rgba($color-info, 0.08);
- color: $color-info;
- font-size: 13px;
+ background: rgba($color-secondary, 0.08);
+ color: $color-secondary;
+ font-size: 12px;
user-select: none;
&::-webkit-details-marker { display: none; }
- &:hover { background: rgba($color-info, 0.14); }
+ &:hover { background: rgba($color-secondary, 0.14); }
.plan-chevron {
margin-left: auto;
font-size: $icon-size-sm;
- color: rgba($color-info, 0.6);
+ color: rgba($color-secondary, 0.6);
transition: transform $motion-fast $motion-ease;
flex-shrink: 0;
}
@@ -890,12 +889,12 @@
}
.plan-body {
- padding: $space-md;
- font-size: 13px;
+ padding: $space-sm $space-md;
+ font-size: 12px;
color: $color-text-medium;
- line-height: 1.6;
- background: rgba($color-info, 0.04);
- border-top: $border-width-base $border-style-base rgba($color-info, 0.3);
+ line-height: 1.5;
+ background: rgba($color-secondary, 0.04);
+ border-top: $border-width-base $border-style-base rgba($color-secondary, 0.3);
p { margin: 0 0 $space-sm; &:last-child { margin-bottom: 0; } }
ul, ol { padding-left: $space-lg; margin-bottom: $space-sm; }
@@ -908,7 +907,7 @@
.tool-card {
border: $border-width-base $border-style-base $border-color-muted;
border-left-width: $border-width-accent;
- margin-bottom: $space-sm;
+ margin-bottom: 8px;
border-left-color: $color-secondary;
overflow: hidden;
font-size: 13px;
@@ -916,12 +915,12 @@
&.is-success { border-left-color: $color-success; }
&.is-error { border-left-color: $color-error; }
- summary {
+ > summary {
list-style: none;
display: flex;
align-items: center;
gap: $space-sm;
- padding: $space-sm $space-md;
+ padding: $space-xs $space-md;
cursor: pointer;
background: $surface-panel-muted;
color: $color-text-medium;
@@ -929,6 +928,17 @@
&::-webkit-details-marker { display: none; }
&:hover { background: $surface-panel-strong; }
+
+ .tool-chevron {
+ font-size: $icon-size-sm;
+ color: $color-text-dark;
+ flex-shrink: 0;
+ transition: transform $motion-fast $motion-ease;
+ }
+ }
+
+ &[open] > summary .tool-chevron {
+ transform: rotate(180deg);
}
.tool-name {
@@ -957,23 +967,6 @@
font-size: $icon-size-sm;
flex-shrink: 0;
}
-
- .tool-emoji {
- font-size: $icon-size-sm;
- line-height: 1;
- flex-shrink: 0;
- }
-
- .tool-chevron {
- font-size: $icon-size-sm;
- color: $color-text-dark;
- flex-shrink: 0;
- transition: transform $motion-fast $motion-ease;
- }
-
- &[open] .tool-chevron {
- transform: rotate(180deg);
- }
}
.tool-card-body {
@@ -1013,11 +1006,162 @@
font-size: 12px;
color: $color-text-medium;
white-space: pre-wrap;
- word-break: break-all;
- max-height: 200px;
- overflow-y: auto;
+ word-break: break-word;
}
+.tool-args {
+ summary {
+ list-style: none;
+ display: flex;
+ align-items: center;
+ gap: $space-sm;
+ padding: $space-xs $space-md;
+ cursor: pointer;
+ user-select: none;
+ font-size: 12px;
+ color: $color-text-medium;
+ background: $surface-panel-muted;
+
+ &::-webkit-details-marker { display: none; }
+ &:hover { background: $surface-panel-strong; }
+
+ .tool-chevron {
+ margin-left: auto;
+ font-size: $icon-size-sm;
+ color: $color-text-dark;
+ transition: transform $motion-fast $motion-ease;
+ }
+ }
+
+ &[open] > summary .tool-chevron {
+ transform: rotate(180deg);
+ }
+}
+
+.tool-cards-summary {
+ display: inline-flex;
+ align-items: center;
+ gap: $space-sm;
+ padding: $space-xs 0;
+}
+
+.tool-fade-item {
+ transition: none;
+}
+
+.tool-fade-enter-active,
+.tool-fade-leave-active {
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+.tool-fade-enter-from {
+ opacity: 0;
+ transform: translateY(-4px);
+}
+
+.tool-fade-leave-to {
+ opacity: 0;
+}
+
+// Smooth
expand/collapse (modern browsers)
+@supports (interpolate-size: allow-keywords) {
+ details {
+ interpolate-size: allow-keywords;
+ }
+
+ details::details-content {
+ block-size: 0;
+ overflow: clip;
+ transition: block-size 0.25s ease, content-visibility 0.25s ease;
+ transition-behavior: allow-discrete;
+ }
+
+ details[open]::details-content {
+ block-size: auto;
+ }
+}
+
+.tool-result {
+ font-size: 12px;
+ line-height: 1.5;
+ color: $color-text-medium;
+
+ // Markdown content inside tool results uses normal font
+ h1, h2, h3, h4 { color: $color-text-light; margin: $space-sm 0; }
+ p { margin: $space-xs 0; }
+ ul, ol { padding-left: $space-lg; margin: $space-xs 0; }
+ li { margin-bottom: 2px; }
+ blockquote {
+ padding-left: $space-sm;
+ border-left: 2px solid $color-secondary;
+ color: $color-text-medium;
+ margin: $space-xs 0;
+ }
+ code {
+ font-family: "IBM Plex Mono", monospace;
+ background: $surface-panel-strong;
+ padding: 1px 4px;
+ color: $color-cyan;
+ font-size: 11px;
+ }
+ pre {
+ margin: $space-xs 0;
+ background: $surface-panel;
+ border: $border-width-base $border-style-base $border-color-muted;
+ overflow: hidden;
+ code {
+ display: block;
+ padding: $space-sm;
+ overflow-x: auto;
+ }
+ }
+}
+
+.result-line {
+ font-family: "IBM Plex Mono", monospace;
+ padding: 1px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.json-object,
+.json-array {
+ font-family: "IBM Plex Mono", monospace;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ padding: $space-xs 0;
+}
+
+.json-item {
+ display: flex;
+ align-items: baseline;
+ gap: $space-xs;
+ padding: 1px 0;
+}
+
+.json-key {
+ color: $color-text-light;
+ font-weight: 500;
+ flex-shrink: 0;
+}
+
+.json-sep {
+ color: $color-text-dark;
+}
+
+.json-value {
+ color: $color-text-medium;
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+
+.json-string { color: $color-success; }
+.json-number { color: $color-warning; }
+.json-bool { color: $color-primary; }
+.json-null { color: $color-text-dark; font-style: italic; }
+.json-empty { color: $color-text-dark; font-style: italic; }
+
// ─── Subagent steps ────────────────────────────────────────────────────────
.subagent-steps {
display: flex;
@@ -1038,6 +1182,71 @@
background: $surface-panel-muted;
}
+.subagent-step {
+ border: $border-width-base $border-style-base $border-color-muted;
+ border-left-width: $border-width-accent;
+ border-left-color: $color-secondary;
+ overflow: hidden;
+ margin-bottom: 8px;
+ font-size: 12px;
+
+ &.is-success { border-left-color: $color-success; }
+ &.is-error { border-left-color: $color-error; }
+
+ summary {
+ list-style: none;
+ display: flex;
+ align-items: center;
+ gap: $space-sm;
+ padding: $space-xs $space-md;
+ cursor: pointer;
+ background: $surface-panel-muted;
+ color: $color-text-medium;
+ user-select: none;
+
+ &::-webkit-details-marker { display: none; }
+ &:hover { background: $surface-panel-strong; }
+ }
+
+ .tool-name {
+ flex: 1;
+ font-family: "IBM Plex Mono", monospace;
+ font-size: 12px;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .tool-running-time {
+ font-family: "IBM Plex Mono", monospace;
+ font-size: 11px;
+ color: $color-text-dark;
+ white-space: nowrap;
+ }
+
+ .tool-status-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $icon-size-sm;
+ height: $icon-size-sm;
+ font-size: $icon-size-sm;
+ flex-shrink: 0;
+ }
+
+ .tool-chevron {
+ font-size: $icon-size-sm;
+ color: $color-text-dark;
+ flex-shrink: 0;
+ transition: transform $motion-fast $motion-ease;
+ }
+
+ &[open] > summary .tool-chevron {
+ transform: rotate(180deg);
+ }
+}
+
// ─── Summary / compression notice ──────────────────────────────────────────
.summary-card {
border: $border-width-base $border-style-base $color-info;
@@ -1108,7 +1317,7 @@
// ─── Input bar ─────────────────────────────────────────────────────────────
.input-bar {
flex-shrink: 0;
- padding: $space-sm $space-md $space-md;
+ padding: 8px 15px;
background: $surface-page;
border-top: $border-width-base $border-style-base $border-color-muted;
}
@@ -1137,6 +1346,9 @@
flex-wrap: wrap;
gap: $space-xs;
margin-bottom: $space-xs;
+ max-width: 920px;
+ margin-left: auto;
+ margin-right: auto;
}
.file-preview-image {
@@ -1463,7 +1675,7 @@
}
// Tool card chevron should still rotate (opt-in)
-.tool-card[open] .tool-chevron,
-.thinking-card[open] .tool-chevron {
+.tool-card[open] > summary .tool-chevron,
+.thinking-card[open] > summary .tool-chevron {
transform: rotate(180deg) !important;
}