feat: Animated pixel art ocean floor background for FloatingTranscriptDebug

Replace galaxy background with animated underwater scene featuring water
depth gradient, pixel art sea floor (sand, seaweed, coral, starfish),
rising bubbles with water sway, and swimming pixel art fish. Also update
transcript-debug tool cards styling and add .claude-*/tasks/ to gitignore.
This commit is contained in:
2026-02-19 14:44:25 -06:00
parent 04f3fe053d
commit c8e8e50fd6
12 changed files with 379 additions and 304 deletions

View File

@@ -665,9 +665,77 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
position: relative;
/* Pixel art galaxy: spiral arms, stars, nebula in bottom-right corner */
isolation: isolate;
/* Pixel art animated ocean floor */
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120' shape-rendering='crispEdges'%3E%3C!-- galaxy core --%3E%3Crect x='56' y='56' width='4' height='4' fill='%23fef3c7' opacity='0.35'/%3E%3Crect x='60' y='56' width='4' height='4' fill='%23fde68a' opacity='0.3'/%3E%3Crect x='56' y='60' width='4' height='4' fill='%23fde68a' opacity='0.28'/%3E%3Crect x='60' y='60' width='4' height='4' fill='%23fef9c3' opacity='0.4'/%3E%3Crect x='52' y='56' width='4' height='4' fill='%23c4b5fd' opacity='0.2'/%3E%3Crect x='64' y='56' width='4' height='4' fill='%23a78bfa' opacity='0.18'/%3E%3Crect x='56' y='52' width='4' height='4' fill='%23818cf8' opacity='0.2'/%3E%3Crect x='60' y='64' width='4' height='4' fill='%23c4b5fd' opacity='0.18'/%3E%3C!-- spiral arm top-right --%3E%3Crect x='68' y='48' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='72' y='44' width='4' height='4' fill='%23818cf8' opacity='0.14'/%3E%3Crect x='76' y='40' width='4' height='4' fill='%236366f1' opacity='0.12'/%3E%3Crect x='80' y='38' width='4' height='2' fill='%23818cf8' opacity='0.1'/%3E%3Crect x='84' y='36' width='4' height='2' fill='%236366f1' opacity='0.08'/%3E%3Crect x='88' y='36' width='2' height='2' fill='%23a78bfa' opacity='0.06'/%3E%3C!-- spiral arm bottom-left --%3E%3Crect x='48' y='68' width='4' height='4' fill='%23818cf8' opacity='0.16'/%3E%3Crect x='44' y='72' width='4' height='4' fill='%236366f1' opacity='0.14'/%3E%3Crect x='40' y='76' width='4' height='4' fill='%23818cf8' opacity='0.12'/%3E%3Crect x='36' y='78' width='4' height='2' fill='%236366f1' opacity='0.1'/%3E%3Crect x='32' y='80' width='4' height='2' fill='%23a78bfa' opacity='0.07'/%3E%3C!-- spiral arm top-left --%3E%3Crect x='48' y='48' width='4' height='4' fill='%23c084fc' opacity='0.15'/%3E%3Crect x='44' y='44' width='4' height='4' fill='%23a855f7' opacity='0.12'/%3E%3Crect x='40' y='40' width='4' height='4' fill='%23c084fc' opacity='0.1'/%3E%3Crect x='36' y='38' width='4' height='2' fill='%23a855f7' opacity='0.08'/%3E%3C!-- spiral arm bottom-right --%3E%3Crect x='68' y='68' width='4' height='4' fill='%23a855f7' opacity='0.14'/%3E%3Crect x='72' y='72' width='4' height='4' fill='%23c084fc' opacity='0.12'/%3E%3Crect x='76' y='76' width='4' height='4' fill='%23a855f7' opacity='0.1'/%3E%3Crect x='80' y='78' width='4' height='2' fill='%23c084fc' opacity='0.07'/%3E%3C!-- nebula clouds --%3E%3Crect x='50' y='40' width='8' height='2' fill='%23f0abfc' opacity='0.07'/%3E%3Crect x='66' y='62' width='6' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3Crect x='42' y='64' width='6' height='2' fill='%23f0abfc' opacity='0.05'/%3E%3Crect x='64' y='44' width='4' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3C!-- scattered stars --%3E%3Crect x='20' y='18' width='2' height='2' fill='white' opacity='0.2'/%3E%3Crect x='95' y='22' width='2' height='2' fill='white' opacity='0.15'/%3E%3Crect x='14' y='90' width='2' height='2' fill='white' opacity='0.12'/%3E%3Crect x='100' y='88' width='2' height='2' fill='white' opacity='0.18'/%3E%3Crect x='30' y='105' width='2' height='2' fill='%23c4b5fd' opacity='0.1'/%3E%3Crect x='108' y='14' width='2' height='2' fill='%23fde68a' opacity='0.12'/%3E%3Crect x='10' y='50' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='110' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.12'/%3E%3Crect x='70' y='20' width='2' height='2' fill='white' opacity='0.08'/%3E%3Crect x='50' y='100' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='85' y='105' width='2' height='2' fill='%23fde68a' opacity='0.08'/%3E%3Crect x='25' y='30' width='2' height='2' fill='%23c4b5fd' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center / 100% 100%;
/* Subtle light rays from surface */
repeating-linear-gradient(
-25deg,
transparent 0px,
transparent 60px,
rgba(103, 232, 249, 0.018) 60px,
rgba(103, 232, 249, 0.018) 63px,
transparent 63px,
transparent 180px
),
/* Water depth gradient */
linear-gradient(
180deg,
rgba(0, 8, 28, 0.96) 0%,
rgba(0, 18, 48, 0.94) 20%,
rgba(0, 30, 60, 0.92) 40%,
rgba(2, 45, 72, 0.88) 60%,
rgba(5, 55, 70, 0.85) 75%,
rgba(12, 58, 58, 0.82) 88%,
rgba(30, 55, 40, 0.78) 100%
),
/* Pixel art sea floor: sand, seaweed, coral, rocks, starfish, shell */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='120' viewBox='0 0 240 120' shape-rendering='crispEdges'%3E%3Crect x='0' y='88' width='240' height='32' fill='%23c2a060' opacity='0.6'/%3E%3Crect x='0' y='86' width='240' height='4' fill='%23d4b878' opacity='0.5'/%3E%3Crect x='10' y='92' width='20' height='2' fill='%23a89048' opacity='0.3'/%3E%3Crect x='50' y='96' width='16' height='2' fill='%23b8a060' opacity='0.25'/%3E%3Crect x='90' y='94' width='24' height='2' fill='%23a89048' opacity='0.3'/%3E%3Crect x='140' y='98' width='20' height='2' fill='%23b8a060' opacity='0.25'/%3E%3Crect x='190' y='92' width='18' height='2' fill='%23a89048' opacity='0.28'/%3E%3Crect x='15' y='80' width='12' height='8' fill='%23475569' opacity='0.55'/%3E%3Crect x='17' y='78' width='8' height='4' fill='%2364748b' opacity='0.45'/%3E%3Crect x='19' y='76' width='4' height='4' fill='%23718096' opacity='0.35'/%3E%3Crect x='45' y='48' width='2' height='40' fill='%2316a34a' opacity='0.55'/%3E%3Crect x='47' y='44' width='2' height='12' fill='%2322c55e' opacity='0.5'/%3E%3Crect x='43' y='52' width='2' height='8' fill='%2315803d' opacity='0.45'/%3E%3Crect x='49' y='40' width='2' height='8' fill='%2322c55e' opacity='0.4'/%3E%3Crect x='41' y='56' width='2' height='6' fill='%2316a34a' opacity='0.35'/%3E%3Crect x='75' y='72' width='2' height='16' fill='%23f87171' opacity='0.5'/%3E%3Crect x='73' y='68' width='2' height='8' fill='%23fb7185' opacity='0.45'/%3E%3Crect x='77' y='70' width='2' height='6' fill='%23f472b6' opacity='0.4'/%3E%3Crect x='71' y='66' width='2' height='4' fill='%23f87171' opacity='0.35'/%3E%3Crect x='79' y='68' width='2' height='4' fill='%23fb7185' opacity='0.35'/%3E%3Crect x='69' y='68' width='2' height='2' fill='%23f472b6' opacity='0.3'/%3E%3Crect x='81' y='70' width='2' height='2' fill='%23f87171' opacity='0.3'/%3E%3Crect x='105' y='62' width='2' height='26' fill='%2322c55e' opacity='0.5'/%3E%3Crect x='107' y='58' width='2' height='10' fill='%2316a34a' opacity='0.45'/%3E%3Crect x='103' y='66' width='2' height='6' fill='%2315803d' opacity='0.4'/%3E%3Crect x='125' y='86' width='2' height='8' fill='%23f97316' opacity='0.5'/%3E%3Crect x='121' y='88' width='10' height='2' fill='%23f97316' opacity='0.5'/%3E%3Crect x='123' y='84' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='129' y='84' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='121' y='92' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='131' y='92' width='2' height='2' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='150' y='50' width='2' height='38' fill='%2316a34a' opacity='0.5'/%3E%3Crect x='152' y='46' width='2' height='10' fill='%2322c55e' opacity='0.45'/%3E%3Crect x='148' y='54' width='2' height='8' fill='%2315803d' opacity='0.4'/%3E%3Crect x='154' y='42' width='2' height='8' fill='%2322c55e' opacity='0.35'/%3E%3Crect x='180' y='76' width='2' height='12' fill='%23818cf8' opacity='0.45'/%3E%3Crect x='178' y='72' width='2' height='6' fill='%23a78bfa' opacity='0.4'/%3E%3Crect x='182' y='74' width='2' height='4' fill='%23c084fc' opacity='0.35'/%3E%3Crect x='176' y='74' width='2' height='2' fill='%23818cf8' opacity='0.3'/%3E%3Crect x='184' y='76' width='2' height='2' fill='%23a78bfa' opacity='0.3'/%3E%3Crect x='210' y='86' width='6' height='4' fill='%23fef3c7' opacity='0.4'/%3E%3Crect x='212' y='84' width='4' height='2' fill='%23fde68a' opacity='0.35'/%3E%3Crect x='214' y='82' width='2' height='2' fill='%23fef3c7' opacity='0.3'/%3E%3Crect x='220' y='82' width='10' height='6' fill='%23475569' opacity='0.5'/%3E%3Crect x='222' y='80' width='6' height='4' fill='%2364748b' opacity='0.4'/%3E%3Crect x='35' y='90' width='2' height='2' fill='%2364748b' opacity='0.3'/%3E%3Crect x='60' y='92' width='2' height='2' fill='%2364748b' opacity='0.25'/%3E%3Crect x='95' y='90' width='2' height='2' fill='%23475569' opacity='0.3'/%3E%3Crect x='160' y='92' width='2' height='2' fill='%2364748b' opacity='0.25'/%3E%3Crect x='200' y='90' width='2' height='2' fill='%23475569' opacity='0.28'/%3E%3C/svg%3E") repeat-x bottom center / 240px 120px;
}
/* Animated bubbles rising through the water */
.content::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: -1;
background:
/* Bubble layer 1 - larger, slower */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='200' viewBox='0 0 80 200' shape-rendering='crispEdges'%3E%3Crect x='20' y='30' width='4' height='4' fill='%2367e8f9' opacity='0.22'/%3E%3Crect x='60' y='90' width='4' height='4' fill='%2367e8f9' opacity='0.18'/%3E%3Crect x='35' y='150' width='4' height='4' fill='%2367e8f9' opacity='0.24'/%3E%3Crect x='50' y='20' width='2' height='2' fill='white' opacity='0.16'/%3E%3Crect x='10' y='70' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='70' y='130' width='2' height='2' fill='white' opacity='0.16'/%3E%3Crect x='25' y='180' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='40' y='50' width='2' height='2' fill='%2367e8f9' opacity='0.1'/%3E%3Crect x='5' y='110' width='2' height='2' fill='%2367e8f9' opacity='0.09'/%3E%3Crect x='55' y='170' width='2' height='2' fill='white' opacity='0.11'/%3E%3C/svg%3E") repeat / 80px 200px,
/* Bubble layer 2 - smaller, different rhythm */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='280' viewBox='0 0 100 280' shape-rendering='crispEdges'%3E%3Crect x='30' y='40' width='4' height='4' fill='%2367e8f9' opacity='0.16'/%3E%3Crect x='75' y='110' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='15' y='180' width='4' height='4' fill='%2367e8f9' opacity='0.2'/%3E%3Crect x='55' y='240' width='2' height='2' fill='white' opacity='0.11'/%3E%3Crect x='85' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.1'/%3E%3Crect x='45' y='150' width='2' height='2' fill='white' opacity='0.13'/%3E%3Crect x='10' y='260' width='2' height='2' fill='%2367e8f9' opacity='0.09'/%3E%3Crect x='65' y='20' width='2' height='2' fill='white' opacity='0.11'/%3E%3C/svg%3E") repeat / 100px 280px;
animation: sea-bubbles 14s linear infinite, water-sway 8s ease-in-out infinite alternate;
}
/* Animated pixel art fish swimming across */
.content::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
z-index: -1;
background:
/* Fish 1: orange tropical fish (right-facing, swims left→right) */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='10' viewBox='0 0 16 10' shape-rendering='crispEdges'%3E%3Crect x='0' y='1' width='2' height='2' fill='%23f97316' opacity='0.6'/%3E%3Crect x='0' y='7' width='2' height='2' fill='%23f97316' opacity='0.6'/%3E%3Crect x='2' y='2' width='2' height='6' fill='%23fb923c' opacity='0.6'/%3E%3Crect x='4' y='1' width='8' height='8' fill='%23f97316' opacity='0.55'/%3E%3Crect x='7' y='1' width='2' height='8' fill='%23fef3c7' opacity='0.4'/%3E%3Crect x='5' y='0' width='4' height='1' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='5' y='9' width='4' height='1' fill='%23fb923c' opacity='0.4'/%3E%3Crect x='10' y='3' width='2' height='2' fill='%23000' opacity='0.5'/%3E%3Crect x='11' y='3' width='1' height='1' fill='white' opacity='0.4'/%3E%3Crect x='12' y='5' width='2' height='1' fill='%23000' opacity='0.3'/%3E%3C/svg%3E") no-repeat / 32px 20px,
/* Fish 2: blue fish (left-facing, swims right→left) */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' shape-rendering='crispEdges'%3E%3Crect x='10' y='1' width='2' height='2' fill='%236366f1' opacity='0.5'/%3E%3Crect x='10' y='5' width='2' height='2' fill='%236366f1' opacity='0.5'/%3E%3Crect x='8' y='2' width='2' height='4' fill='%23818cf8' opacity='0.5'/%3E%3Crect x='2' y='1' width='6' height='6' fill='%236366f1' opacity='0.45'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%23000' opacity='0.4'/%3E%3Crect x='2' y='2' width='1' height='1' fill='white' opacity='0.3'/%3E%3Crect x='4' y='0' width='3' height='1' fill='%23818cf8' opacity='0.35'/%3E%3C/svg%3E") no-repeat / 24px 16px;
animation: fish-swim 20s linear infinite;
}
@keyframes sea-bubbles {
from { background-position: 0 0, 40px 0; }
to { background-position: 0 -200px, 0 -280px; }
}
@keyframes water-sway {
from { transform: translateX(-3px); }
to { transform: translateX(3px); }
}
@keyframes fish-swim {
0% { background-position: -40px 25%, calc(100% + 30px) 55%; }
100% { background-position: calc(100% + 40px) 30%, -30px 50%; }
}
/* Override ChatContainer backgrounds for black transparency */

View File

@@ -103,12 +103,9 @@ function formatTokens(n?: number): string {
<style scoped>
.assistant-bubble {
background: transparent;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: none;
border-radius: 12px;
padding: 0.75rem 1rem;
margin-right: 2rem;
border-radius: 0;
padding: 0.5rem 0.25rem;
}
.assistant-bubble.continuation {

View File

@@ -107,6 +107,51 @@ async function copySelected() {
setTimeout(() => (selectedCopied.value = false), 1500)
}
// ── Collapse sections ──
const collapsedSections = ref(new Set<string>())
// For each message, find which user message "owns" it (section leader)
const sectionMap = computed(() => {
const map = new Map<string, string>() // messageUuid → ownerUserUuid
let currentUserUuid: string | null = null
for (const msg of props.conversation.messages) {
if (msg.kind === 'user') {
currentUserUuid = msg.uuid
} else if (currentUserUuid) {
map.set(msg.uuid, currentUserUuid)
}
}
return map
})
// Count of non-user messages per section
const sectionCounts = computed(() => {
const counts = new Map<string, number>()
let currentUserUuid: string | null = null
for (const msg of props.conversation.messages) {
if (msg.kind === 'user') {
currentUserUuid = msg.uuid
if (!counts.has(currentUserUuid)) counts.set(currentUserUuid, 0)
} else if (currentUserUuid) {
counts.set(currentUserUuid, (counts.get(currentUserUuid) || 0) + 1)
}
}
return counts
})
function toggleCollapse(userUuid: string) {
const s = new Set(collapsedSections.value)
if (s.has(userUuid)) s.delete(userUuid)
else s.add(userUuid)
collapsedSections.value = s
}
function isCollapsedChild(msg: { uuid: string; kind: string }): boolean {
if (msg.kind === 'user') return false
const owner = sectionMap.value.get(msg.uuid)
return !!owner && collapsedSections.value.has(owner)
}
// Track messages that just resolved from optimistic → real
// These skip the bounce animation and get a smooth transition instead
const resolvedUuids = ref(new Set<string>())
@@ -207,47 +252,54 @@ function formatDuration(start: string, end: string): string {
</div>
</div>
<div ref="scrollContainer" class="messages-scroll">
<div
<template
v-for="(msg, idx) in conversation.messages"
:key="msg.uuid"
:class="['message-wrapper', {
resolved: resolvedUuids.has(msg.uuid),
selected: selectedUuids.has(msg.uuid),
'assistant-continuation': msg.kind === 'assistant' && idx > 0 && conversation.messages[idx - 1].kind === 'assistant'
}]"
>
<!-- Select checkbox -->
<button
v-if="selectMode && msg.kind !== 'progress'"
:class="['select-checkbox', { checked: selectedUuids.has(msg.uuid) }]"
@click.stop="toggleSelect(msg.uuid)"
:title="selectedUuids.has(msg.uuid) ? 'Deselect' : 'Select'"
<div
v-if="!isCollapsedChild(msg)"
:class="['message-wrapper', {
resolved: resolvedUuids.has(msg.uuid),
selected: selectedUuids.has(msg.uuid),
'assistant-continuation': msg.kind === 'assistant' && idx > 0 && conversation.messages[idx - 1].kind === 'assistant' && !isCollapsedChild(conversation.messages[idx - 1])
}]"
>
<svg v-if="selectedUuids.has(msg.uuid)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
<!-- Select checkbox -->
<button
v-if="selectMode && msg.kind !== 'progress'"
:class="['select-checkbox', { checked: selectedUuids.has(msg.uuid) }]"
@click.stop="toggleSelect(msg.uuid)"
:title="selectedUuids.has(msg.uuid) ? 'Deselect' : 'Select'"
>
<svg v-if="selectedUuids.has(msg.uuid)" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
<div :class="['message-content', { selectable: selectMode && msg.kind !== 'progress' }]" @click="selectMode && msg.kind !== 'progress' && toggleSelect(msg.uuid)">
<UserMessageBubble
v-if="msg.kind === 'user'"
:message="msg"
/>
<AssistantMessageBubble
v-else-if="msg.kind === 'assistant'"
:message="msg"
:show-header="!(idx > 0 && conversation.messages[idx - 1].kind === 'assistant')"
/>
<ProgressEvent
v-else-if="msg.kind === 'progress'"
:group="msg"
/>
<SystemMessage
v-else-if="msg.kind === 'system'"
:message="msg"
/>
<div :class="['message-content', { selectable: selectMode && msg.kind !== 'progress' }]" @click="selectMode && msg.kind !== 'progress' && toggleSelect(msg.uuid)">
<UserMessageBubble
v-if="msg.kind === 'user'"
:message="msg"
:collapsed="collapsedSections.has(msg.uuid)"
:section-count="sectionCounts.get(msg.uuid) || 0"
@toggle-collapse="toggleCollapse(msg.uuid)"
/>
<AssistantMessageBubble
v-else-if="msg.kind === 'assistant'"
:message="msg"
:show-header="!(idx > 0 && conversation.messages[idx - 1].kind === 'assistant')"
/>
<ProgressEvent
v-else-if="msg.kind === 'progress'"
:group="msg"
/>
<SystemMessage
v-else-if="msg.kind === 'system'"
:message="msg"
/>
</div>
</div>
</div>
</template>
</div>
<!-- Selection floating bar -->

View File

@@ -4,6 +4,12 @@ import type { ParsedUserMessage } from '@/types/transcript-debug'
const props = defineProps<{
message: ParsedUserMessage
collapsed?: boolean
sectionCount?: number
}>()
const emit = defineEmits<{
toggleCollapse: []
}>()
const isOptimistic = computed(() => props.message.uuid.startsWith('optimistic-'))
@@ -15,57 +21,151 @@ function formatTime(ts: string): string {
</script>
<template>
<div :class="['user-bubble', { meta: message.isMeta, optimistic: isOptimistic }]">
<div class="bubble-header">
<span class="role-badge">User</span>
<span v-if="message.isMeta" class="meta-badge">meta</span>
<span v-if="isOptimistic" class="sending-badge">
<span class="sending-dot"></span>
<span class="sending-dot"></span>
<span class="sending-dot"></span>
Sending
</span>
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
<div :class="['user-divider', { meta: message.isMeta, optimistic: isOptimistic }]">
<div class="divider-line" />
<div class="divider-content">
<div class="divider-header">
<span class="role-badge">User</span>
<span v-if="message.isMeta" class="meta-badge">meta</span>
<span v-if="isOptimistic" class="sending-badge">
<span class="sending-dot"></span>
<span class="sending-dot"></span>
<span class="sending-dot"></span>
Sending
</span>
<button
v-if="sectionCount && sectionCount > 0"
:class="['collapse-btn', { collapsed }]"
@click.stop="emit('toggleCollapse')"
:title="collapsed ? 'Expand section' : 'Collapse section'"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline :points="collapsed ? '9 18 15 12 9 6' : '6 9 12 15 18 9'" />
</svg>
<span v-if="collapsed" class="collapse-count">{{ sectionCount }}</span>
</button>
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
</div>
<div class="divider-text">{{ message.content }}</div>
</div>
<div class="bubble-content">{{ message.content }}</div>
</div>
</template>
<style scoped>
.user-bubble {
.user-divider {
width: 100%;
margin: 0.75rem 0 0.25rem;
}
.divider-line {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(129, 140, 248, 0.2) 10%, rgba(129, 140, 248, 0.2) 90%, transparent);
margin-bottom: 0.5rem;
}
.divider-content {
padding: 0 0.25rem;
}
.divider-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.role-badge {
font-size: 10px;
font-weight: 700;
color: #818cf8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.meta-badge {
font-size: 9px;
padding: 0.05rem 0.3rem;
border-radius: 3px;
background: transparent;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: rgba(251, 191, 36, 0.7);
font-weight: 600;
}
.timestamp {
font-size: 10px;
color: var(--text-muted);
margin-left: auto;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.divider-text {
font-size: 13px;
line-height: 1.5;
color: var(--text-primary);
font-weight: 500;
white-space: pre-wrap;
word-break: break-word;
}
/* Meta messages: dimmed */
.user-divider.meta {
opacity: 0.45;
}
.user-divider.meta .divider-line {
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.15) 10%, rgba(251, 191, 36, 0.15) 90%, transparent);
}
/* Collapse button */
.collapse-btn {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 1px 4px;
border: none;
border-radius: 12px;
padding: 0.75rem 1rem;
margin-left: 2rem;
transition: opacity 0.3s, border-color 0.3s, background 0.3s;
}
.user-bubble.meta {
opacity: 0.5;
border-style: dashed;
}
/* Optimistic / sending state */
.user-bubble.optimistic {
opacity: 0.7;
border-color: rgba(99, 102, 241, 0.08);
border-style: dashed;
border-radius: 3px;
background: transparent;
cursor: pointer;
color: var(--text-muted);
transition: all 0.15s;
}
.collapse-btn:hover {
background: rgba(129, 140, 248, 0.1);
color: #818cf8;
}
.collapse-btn svg {
transition: transform 0.2s ease;
}
.collapse-count {
font-size: 9px;
font-weight: 600;
color: var(--text-muted);
font-family: 'SF Mono', 'Fira Code', monospace;
}
.collapse-btn:hover .collapse-count {
color: #818cf8;
}
/* Optimistic / sending */
.user-divider.optimistic {
opacity: 0.6;
}
.user-divider.optimistic .divider-line {
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1) 10%, rgba(99, 102, 241, 0.1) 90%, transparent);
}
.sending-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10px;
font-size: 9px;
color: var(--accent, #6366f1);
font-weight: 500;
padding: 0.1rem 0.4rem;
border-radius: 4px;
background: rgba(99, 102, 241, 0.08);
}
.sending-dot {
@@ -83,42 +183,4 @@ function formatTime(ts: string): string {
0%, 80%, 100% { opacity: 0.2; }
40% { opacity: 1; }
}
.bubble-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.role-badge {
font-size: 11px;
font-weight: 600;
color: #818cf8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.meta-badge {
font-size: 10px;
padding: 0.1rem 0.4rem;
border-radius: 4px;
background: rgba(251, 191, 36, 0.15);
color: #fbbf24;
}
.timestamp {
font-size: 11px;
color: var(--text-muted);
margin-left: auto;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.bubble-content {
font-size: 13px;
line-height: 1.6;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-word;
}
</style>

View File

@@ -17,8 +17,7 @@ const isError = computed(() => props.call.result?.isError ?? false)
const highlightedCommand = computed(() => highlightCode(command.value, 'bash'))
const cmdExpanded = ref(false)
const resultExpanded = ref(false)
const expanded = ref(false)
const cmdPreview = computed(() => {
const c = command.value.replace(/\n/g, ' ').trim()
@@ -28,7 +27,7 @@ const cmdPreview = computed(() => {
<template>
<div :class="['bash-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
@@ -40,32 +39,18 @@ const cmdPreview = computed(() => {
<span v-if="runInBackground" class="info-badge">bg</span>
<span v-if="timeout" class="info-badge">{{ timeout }}ms</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button class="toggle-btn" :class="{ active: cmdExpanded }" @click.stop="cmdExpanded = !cmdExpanded" title="Toggle command">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v18M3 12h18"/>
</svg>
</button>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<!-- Full command -->
<div v-if="cmdExpanded" class="command-section">
<div v-if="description" class="description">{{ description }}</div>
<div class="command-row">
<span class="prompt">$</span>
<pre class="command-text" v-html="highlightedCommand"></pre>
<template v-if="expanded">
<div class="command-section">
<div v-if="description" class="description">{{ description }}</div>
<div class="command-row">
<span class="prompt">$</span>
<pre class="command-text" v-html="highlightedCommand"></pre>
</div>
</div>
</div>
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<ToolResultBlock v-if="call.result" :result="call.result" />
</template>
</div>
</template>
@@ -88,7 +73,9 @@ const cmdPreview = computed(() => {
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(245, 158, 11, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(245, 158, 11, 0.6); flex-shrink: 0; }
.bash-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }

View File

@@ -27,8 +27,7 @@ const ext = computed(() => {
const isError = computed(() => props.call.result?.isError ?? false)
const diffExpanded = ref(false)
const resultExpanded = ref(false)
const expanded = ref(false)
const highlightedOld = computed(() => highlightCode(oldString.value, ext.value || undefined))
const highlightedNew = computed(() => highlightCode(newString.value, ext.value || undefined))
@@ -39,7 +38,7 @@ const newLineCount = computed(() => newString.value.split('\n').length)
<template>
<div :class="['edit-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
@@ -53,36 +52,24 @@ const newLineCount = computed(() => newString.value.split('\n').length)
<span class="diff-removed">-{{ oldLineCount }}</span>
<span class="diff-added">+{{ newLineCount }}</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button class="toggle-btn" :class="{ active: diffExpanded }" @click.stop="diffExpanded = !diffExpanded" title="Toggle diff">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v18M3 12h18"/>
</svg>
</button>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<!-- Diff view -->
<div v-if="diffExpanded" class="diff-content">
<div class="diff-block removed">
<div class="diff-label"><span class="diff-sign">-</span> old</div>
<pre class="diff-code" v-html="highlightedOld"></pre>
<template v-if="expanded">
<!-- Diff view -->
<div class="diff-content">
<div class="diff-block removed">
<div class="diff-label"><span class="diff-sign">-</span> old</div>
<pre class="diff-code" v-html="highlightedOld"></pre>
</div>
<div class="diff-block added">
<div class="diff-label"><span class="diff-sign">+</span> new</div>
<pre class="diff-code" v-html="highlightedNew"></pre>
</div>
</div>
<div class="diff-block added">
<div class="diff-label"><span class="diff-sign">+</span> new</div>
<pre class="diff-code" v-html="highlightedNew"></pre>
</div>
</div>
<!-- Result -->
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<!-- Result -->
<ToolResultBlock v-if="call.result" :result="call.result" />
</template>
</div>
</template>
@@ -105,8 +92,11 @@ const newLineCount = computed(() => newString.value.split('\n').length)
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(99, 102, 241, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(99, 102, 241, 0.6); flex-shrink: 0; }
.edit-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }
@@ -147,26 +137,6 @@ const newLineCount = computed(() => newString.value.split('\n').length)
.diff-added { color: rgba(34, 197, 94, 0.7); }
.error-badge { color: rgba(239, 68, 68, 0.7); }
.header-spacer { flex: 1; }
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
flex-shrink: 0;
opacity: 0.5;
transition: all 0.15s;
}
.toggle-btn:hover { opacity: 0.8; }
.toggle-btn.active { opacity: 1; color: rgba(99, 102, 241, 0.8); }
/* Diff */
.diff-content { border-top: 1px solid rgba(255, 255, 255, 0.04); }

View File

@@ -19,7 +19,7 @@ const fileCount = computed(() => {
return content.split('\n').filter(l => l.trim()).length
})
const resultExpanded = ref(false)
const expanded = ref(false)
const shortPath = computed(() => {
if (!path.value) return ''
@@ -29,7 +29,7 @@ const shortPath = computed(() => {
<template>
<div :class="['glob-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
@@ -40,18 +40,9 @@ const shortPath = computed(() => {
<span v-if="path" class="scope-badge" :title="path">{{ shortPath }}</span>
<span v-if="fileCount != null && !isError" class="match-count">{{ fileCount }}</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<ToolResultBlock v-if="expanded && call.result" :result="call.result" />
</div>
</template>
@@ -74,7 +65,9 @@ const shortPath = computed(() => {
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(251, 191, 36, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(251, 191, 36, 0.6); flex-shrink: 0; }
.glob-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }

View File

@@ -42,8 +42,7 @@ const flags = computed(() => {
return f
})
const detailsExpanded = ref(false)
const resultExpanded = ref(false)
const expanded = ref(false)
const shortPath = computed(() => {
if (!path.value) return ''
@@ -53,7 +52,7 @@ const shortPath = computed(() => {
<template>
<div :class="['grep-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
@@ -66,31 +65,17 @@ const shortPath = computed(() => {
<span v-if="type" class="scope-badge type">{{ type }}</span>
<span v-if="matchCount != null && !isError" class="match-count">{{ matchCount }}</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button v-if="path || context || headLimit || outputMode !== 'files_with_matches'" class="toggle-btn" :class="{ active: detailsExpanded }" @click.stop="detailsExpanded = !detailsExpanded" title="Toggle details">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
</svg>
</button>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<!-- Details row -->
<div v-if="detailsExpanded" class="details-row">
<span v-if="path" class="detail-badge path" :title="path">{{ shortPath }}</span>
<span v-if="outputMode !== 'files_with_matches'" class="detail-badge">{{ outputMode }}</span>
<span v-if="context" class="detail-badge">{{ context }}</span>
<span v-if="headLimit" class="detail-badge">head {{ headLimit }}</span>
</div>
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<template v-if="expanded">
<div v-if="path || context || headLimit || outputMode !== 'files_with_matches'" class="details-row">
<span v-if="path" class="detail-badge path" :title="path">{{ shortPath }}</span>
<span v-if="outputMode !== 'files_with_matches'" class="detail-badge">{{ outputMode }}</span>
<span v-if="context" class="detail-badge">{{ context }}</span>
<span v-if="headLimit" class="detail-badge">head {{ headLimit }}</span>
</div>
<ToolResultBlock v-if="call.result" :result="call.result" />
</template>
</div>
</template>
@@ -113,7 +98,9 @@ const shortPath = computed(() => {
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(236, 72, 153, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(236, 72, 153, 0.6); flex-shrink: 0; }
.grep-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }

View File

@@ -26,12 +26,12 @@ const ext = computed(() => {
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : ''
})
const resultExpanded = ref(false)
const expanded = ref(false)
</script>
<template>
<div :class="['read-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
@@ -45,18 +45,9 @@ const resultExpanded = ref(false)
<span v-if="limit != null" class="info-badge">{{ limit }}L</span>
<span v-if="pages" class="info-badge">p{{ pages }}</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<ToolResultBlock v-if="expanded && call.result" :result="call.result" />
</div>
</template>
@@ -79,7 +70,9 @@ const resultExpanded = ref(false)
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(6, 182, 212, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(6, 182, 212, 0.6); flex-shrink: 0; }
.read-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }

View File

@@ -21,10 +21,7 @@ const subagentType = computed(() => (props.call.input?.subagent_type as string)
const model = computed(() => (props.call.input?.model as string) || '')
const runInBackground = computed(() => props.call.input?.run_in_background as boolean | undefined)
// Toggles
const showPrompt = ref(false)
const showResult = ref(false)
const showDesc = ref(false)
const expanded = ref(false)
// ---- Extract main topics from result ----
interface TopicItem {
@@ -124,9 +121,9 @@ const cardColor = computed(() => {
}
})
const hasDesc = computed(() => description.value.length > 0)
const hasPrompt = computed(() => prompt.value.length > 0)
const hasResult = computed(() => !!props.call.result)
const hasExpandable = computed(() =>
description.value.length > 0 || prompt.value.length > 0 || !!props.call.result
)
// Brief for body (only when no topics to show)
const descBrief = computed(() => {
@@ -143,7 +140,7 @@ const hasBody = computed(() =>
<template>
<div :class="['task-card', { error: isError }]" :style="{ '--card-color': cardColor }">
<!-- HEADER -->
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg v-if="toolName === 'Task'" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
@@ -171,25 +168,6 @@ const hasBody = computed(() =>
<span v-if="model" class="model-badge">{{ model }}</span>
<span v-if="runInBackground" class="bg-badge">bg</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer" />
<!-- Toggle buttons -->
<button v-if="hasDesc" class="toggle-btn" :class="{ active: showDesc }" @click.stop="showDesc = !showDesc" title="Description">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
</button>
<button v-if="hasPrompt" class="toggle-btn" :class="{ active: showPrompt }" @click.stop="showPrompt = !showPrompt" title="Prompt">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
<button v-if="hasResult" class="toggle-btn" :class="{ active: showResult }" @click.stop="showResult = !showResult" title="Result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<!-- BODY: subject / brief -->
@@ -208,20 +186,18 @@ const hasBody = computed(() =>
</div>
</div>
<!-- EXPANDED: Description -->
<div v-if="showDesc && description" class="expand-section">
<pre class="expand-content">{{ description }}</pre>
</div>
<!-- EXPANDED: Prompt -->
<div v-if="showPrompt && prompt" class="expand-section">
<pre class="expand-content">{{ prompt }}</pre>
</div>
<!-- EXPANDED: Raw result -->
<div v-if="showResult && call.result" class="expand-section result-section">
<ToolResultBlock :result="call.result" />
</div>
<!-- EXPANDED: all details -->
<template v-if="expanded">
<div v-if="description" class="expand-section">
<pre class="expand-content">{{ description }}</pre>
</div>
<div v-if="prompt" class="expand-section">
<pre class="expand-content">{{ prompt }}</pre>
</div>
<div v-if="call.result" class="expand-section result-section">
<ToolResultBlock :result="call.result" />
</div>
</template>
</div>
</template>
@@ -245,7 +221,9 @@ const hasBody = computed(() =>
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: color-mix(in srgb, var(--card-color) 4%, transparent); }
.card-icon { display: flex; align-items: center; color: color-mix(in srgb, var(--card-color) 60%, transparent); flex-shrink: 0; }
.task-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }

View File

@@ -25,8 +25,7 @@ const ext = computed(() => {
const isError = computed(() => props.call.result?.isError ?? false)
const contentExpanded = ref(false)
const resultExpanded = ref(false)
const expanded = ref(false)
const highlightedContent = computed(() => highlightCode(content.value, ext.value || undefined))
@@ -35,7 +34,7 @@ const lineCount = computed(() => content.value.split('\n').length)
<template>
<div :class="['write-card', { error: isError }]">
<div class="card-header">
<div class="card-header" @click.stop="expanded = !expanded">
<span class="card-icon">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
@@ -49,28 +48,14 @@ const lineCount = computed(() => content.value.split('\n').length)
<span v-if="ext" class="ext-badge">.{{ ext }}</span>
<span class="line-meta">{{ lineCount }}L</span>
<span v-if="isError" class="error-badge">err</span>
<span class="header-spacer"></span>
<button class="toggle-btn" :class="{ active: contentExpanded }" @click.stop="contentExpanded = !contentExpanded" title="Toggle content">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v18M3 12h18"/>
</svg>
</button>
<button v-if="call.result" class="toggle-btn" :class="{ active: resultExpanded }" @click.stop="resultExpanded = !resultExpanded" title="Toggle result">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</button>
</div>
<!-- Content preview -->
<div v-if="contentExpanded" class="content-section">
<pre class="content-pre" v-html="highlightedContent"></pre>
</div>
<ToolResultBlock v-if="resultExpanded && call.result" :result="call.result" />
<template v-if="expanded">
<div class="content-section">
<pre class="content-pre" v-html="highlightedContent"></pre>
</div>
<ToolResultBlock v-if="call.result" :result="call.result" />
</template>
</div>
</template>
@@ -93,7 +78,9 @@ const lineCount = computed(() => content.value.split('\n').length)
padding: 0.3rem 0.6rem;
background: transparent;
min-height: 28px;
cursor: pointer;
}
.card-header:hover { background: rgba(34, 197, 94, 0.04); }
.card-icon { display: flex; align-items: center; color: rgba(34, 197, 94, 0.6); flex-shrink: 0; }
.write-card.error .card-icon { color: rgba(239, 68, 68, 0.6); }