From 06b48ebda3d99db17c64480c51bb5b350b615eab Mon Sep 17 00:00:00 2001 From: josedario87 Date: Thu, 19 Feb 2026 03:12:17 -0600 Subject: [PATCH] feat: Compact transparent tool cards with grouped assistant messages - Redesign all tool cards (Edit, Read, Bash, Grep, Glob, Write) to be compact single-line headers with inline key info and toggle buttons - Make cards and bubbles fully transparent with subtle color tints - Remove borders, use only left accent bar per card type - Color-code numbers: red for removals, green for additions/counts - Simplify ToolResultBlock to render content directly without toggle - Group consecutive assistant messages, showing header only on first - Remove borders from assistant and user message bubbles --- .../AssistantMessageBubble.vue | 17 +- .../transcript-debug/ChatContainer.vue | 13 +- .../transcript-debug/ToolResultBlock.vue | 92 +--- .../transcript-debug/UserMessageBubble.vue | 12 +- .../transcript-debug/toolCards/BashCard.vue | 189 ++++--- .../transcript-debug/toolCards/EditCard.vue | 269 ++++----- .../toolCards/ExitPlanModeCard.vue | 2 +- .../transcript-debug/toolCards/GlobCard.vue | 213 ++++--- .../transcript-debug/toolCards/GrepCard.vue | 263 +++++---- .../transcript-debug/toolCards/ReadCard.vue | 164 +++--- .../transcript-debug/toolCards/TaskCard.vue | 519 +++++++++--------- .../transcript-debug/toolCards/WriteCard.vue | 187 +++---- 12 files changed, 868 insertions(+), 1072 deletions(-) diff --git a/frontend/src/components/transcript-debug/AssistantMessageBubble.vue b/frontend/src/components/transcript-debug/AssistantMessageBubble.vue index 674372f..195d356 100644 --- a/frontend/src/components/transcript-debug/AssistantMessageBubble.vue +++ b/frontend/src/components/transcript-debug/AssistantMessageBubble.vue @@ -19,6 +19,7 @@ const TASK_TOOLS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'Task const props = defineProps<{ message: ParsedAssistantMessage + showHeader?: boolean }>() // Filter out empty text blocks (streaming placeholders) @@ -47,8 +48,8 @@ function formatTokens(n?: number): string { diff --git a/frontend/src/components/transcript-debug/toolCards/GrepCard.vue b/frontend/src/components/transcript-debug/toolCards/GrepCard.vue index 10d72a7..e58dd96 100644 --- a/frontend/src/components/transcript-debug/toolCards/GrepCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/GrepCard.vue @@ -28,7 +28,6 @@ const context = computed(() => { const isError = computed(() => props.call.result?.isError ?? false) -// Count matches from result const matchCount = computed(() => { if (!props.call.result?.content) return null const content = props.call.result.content.trim() @@ -36,193 +35,185 @@ const matchCount = computed(() => { return content.split('\n').filter(l => l.trim()).length }) +const flags = computed(() => { + let f = '' + if (caseInsensitive.value) f += 'i' + if (multiline.value) f += 'm' + return f +}) + const detailsExpanded = ref(false) +const resultExpanded = ref(false) + +const shortPath = computed(() => { + if (!path.value) return '' + return path.value.replace(/\\/g, '/').split('/').slice(-2).join('/') +}) diff --git a/frontend/src/components/transcript-debug/toolCards/ReadCard.vue b/frontend/src/components/transcript-debug/toolCards/ReadCard.vue index 275090e..d70d2f2 100644 --- a/frontend/src/components/transcript-debug/toolCards/ReadCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/ReadCard.vue @@ -20,7 +20,6 @@ const pages = computed(() => props.call.input?.pages as string | undefined) const isError = computed(() => props.call.result?.isError ?? false) -// Detect file extension for icon hint const ext = computed(() => { const name = fileName.value const dot = name.lastIndexOf('.') @@ -34,136 +33,109 @@ const resultExpanded = ref(false)
- + Read + {{ fileName }} .{{ ext }} - error + @{{ offset }} + {{ limit }}L + p{{ pages }} + err + + + +
-
-
- {{ filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') }}/ - {{ fileName }} -
-
- offset: {{ offset }} - limit: {{ limit }} - pages: {{ pages }} -
-
- - - +
diff --git a/frontend/src/components/transcript-debug/toolCards/TaskCard.vue b/frontend/src/components/transcript-debug/toolCards/TaskCard.vue index 4d3538d..cd2ed66 100644 --- a/frontend/src/components/transcript-debug/toolCards/TaskCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/TaskCard.vue @@ -8,11 +8,9 @@ const props = defineProps<{ }>() const isError = computed(() => props.call.result?.isError ?? false) - -// Detect which Task-family tool this is const toolName = computed(() => props.call.name) -// Common fields across Task tools +// Common fields const taskId = computed(() => (props.call.input?.taskId as string) || '') const subject = computed(() => (props.call.input?.subject as string) || '') const description = computed(() => (props.call.input?.description as string) || '') @@ -20,338 +18,341 @@ const activeForm = computed(() => (props.call.input?.activeForm as string) || '' const status = computed(() => (props.call.input?.status as string) || '') const prompt = computed(() => (props.call.input?.prompt as string) || '') const subagentType = computed(() => (props.call.input?.subagent_type as string) || '') - -// For Task (subagent launcher) -const taskDescription = computed(() => (props.call.input?.description as string) || '') const model = computed(() => (props.call.input?.model as string) || '') const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined) -// Status styling -const statusColor = computed(() => { - switch (status.value) { +// Toggles +const showPrompt = ref(false) +const showResult = ref(false) +const showDesc = ref(false) + +// ---- Extract main topics from result ---- +interface TopicItem { + label: string + status?: string // for TaskList JSON items + id?: string +} + +const mainTopics = computed(() => { + if (!props.call.result?.content) return [] + const content = props.call.result.content.trim() + + // 1) Try JSON (TaskList array / TaskGet single object) + try { + const data = JSON.parse(content) + if (Array.isArray(data)) { + return data.map((t: any) => ({ + label: t.subject || t.description || JSON.stringify(t).slice(0, 60), + status: t.status, + id: t.id, + })) + } + if (data && typeof data === 'object' && (data.subject || data.id)) { + return [{ + label: data.subject || data.description || '', + status: data.status, + id: data.id, + }] + } + } catch { /* not JSON, parse as text */ } + + // 2) Text result: extract headings, numbered/bullet lists, bold items + const lines = content.split('\n') + const topics: TopicItem[] = [] + const seen = new Set() + + for (const raw of lines) { + const line = raw.trim() + if (!line) continue + + let label = '' + // Markdown headings: ## Foo or ### Foo + if (/^#{1,4}\s+/.test(line)) { + label = line.replace(/^#+\s+/, '').replace(/\*\*/g, '').trim() + } + // Numbered list: 1. Foo, 2. Foo + else if (/^\d+[\.\)]\s+/.test(line)) { + label = line.replace(/^\d+[\.\)]\s+/, '').replace(/\*\*/g, '').trim() + } + // Bullet list: - Foo, * Foo + else if (/^[-*]\s+/.test(line)) { + label = line.replace(/^[-*]\s+/, '').replace(/\*\*/g, '').trim() + } + + if (label && label.length > 2 && !seen.has(label)) { + seen.add(label) + topics.push({ label: label.length > 90 ? label.slice(0, 90) + '...' : label }) + } + if (topics.length >= 12) break + } + return topics +}) + +const hasTopics = computed(() => mainTopics.value.length > 0) + +// Status helpers +const statusColorFor = (s: string) => { + switch (s) { case 'completed': return '#22c55e' case 'in_progress': return '#f59e0b' case 'pending': return '#64748b' case 'deleted': return '#ef4444' - default: return '#64748b' + default: return 'var(--card-color)' } -}) +} +const statusColor = computed(() => statusColorFor(status.value)) const statusIcon = computed(() => { switch (status.value) { - case 'completed': return '✓' - case 'in_progress': return '→' - case 'pending': return '○' - case 'deleted': return '✗' - default: return '•' + case 'completed': return '\u2713' + case 'in_progress': return '\u25CF' + case 'pending': return '\u25CB' + case 'deleted': return '\u2717' + default: return '\u2022' } }) -// Card color based on tool +// Card color const cardColor = computed(() => { switch (toolName.value) { - case 'Task': return '#0ea5e9' // blue - agent launch - case 'TaskCreate': return '#22c55e' // green - create - case 'TaskUpdate': return '#a855f7' // purple - update - case 'TaskGet': return '#06b6d4' // cyan - read - case 'TaskList': return '#64748b' // gray - list + case 'Task': return '#0ea5e9' + case 'TaskCreate': return '#22c55e' + case 'TaskUpdate': return '#a855f7' + case 'TaskGet': return '#06b6d4' + case 'TaskList': return '#64748b' default: return '#64748b' } }) -const cardIcon = computed(() => toolName.value) +const hasDesc = computed(() => description.value.length > 0) +const hasPrompt = computed(() => prompt.value.length > 0) +const hasResult = computed(() => !!props.call.result) -const descExpanded = ref(false) -const promptExpanded = ref(false) +// Brief for body (only when no topics to show) +const descBrief = computed(() => { + if (!description.value) return '' + const first = description.value.split('\n')[0] + return first.length > 80 ? first.slice(0, 80) + '...' : first +}) + +const hasBody = computed(() => + subject.value || activeForm.value || (!hasTopics.value && descBrief.value) +) diff --git a/frontend/src/components/transcript-debug/toolCards/WriteCard.vue b/frontend/src/components/transcript-debug/toolCards/WriteCard.vue index 941f1b5..4fc3cd5 100644 --- a/frontend/src/components/transcript-debug/toolCards/WriteCard.vue +++ b/frontend/src/components/transcript-debug/toolCards/WriteCard.vue @@ -26,6 +26,7 @@ const ext = computed(() => { const isError = computed(() => props.call.result?.isError ?? false) const contentExpanded = ref(false) +const resultExpanded = ref(false) const highlightedContent = computed(() => highlightCode(content.value, ext.value || undefined)) @@ -36,7 +37,7 @@ const lineCount = computed(() => content.value.split('\n').length)
- + @@ -44,167 +45,129 @@ const lineCount = computed(() => content.value.split('\n').length) Write + {{ fileName }} .{{ ext }} - error -
+ {{ lineCount }}L + err -
-
- {{ filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/') }}/ - {{ fileName }} -
+ + + +
-
- -

+    
+

     
- - +