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

@@ -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 -->