From 18378adb770695395028c4894026c674fef885cd Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 20 Feb 2026 14:28:37 -0600 Subject: [PATCH] feat: TurnEndDivider with prismarine floor, elevated FAB with bubbles - Add TurnEndDivider component with pixel art ocean reef divider - Parser merges stop_hook_summary + turn_duration into single turn_end - Prismarine-inspired mosaic floor with SVG pattern and crystal highlights - Animated duration badge with underwater glow effect - Move transcript FAB to bottom-right, add elevated multi-layer shadow - Add occasional bubble particles rising from FAB button - Prevent long-touch selection on FAB (contextmenu + touch-callout) - FAB stays fixed on mobile when terminal sheet opens --- frontend/src/App.vue | 177 +++++++--- .../components/FloatingTranscriptDebug.vue | 46 ++- .../transcript-debug/ChatContainer.vue | 31 ++ .../transcript-debug/TurnEndDivider.vue | 302 ++++++++++++++++++ .../components/transcript-debug/UserInput.vue | 3 +- .../src/components/transcript-debug/index.ts | 2 + .../transcript-debug/useTranscriptDebug.ts | 60 +++- frontend/src/types/transcript-debug.ts | 1 + 8 files changed, 558 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/transcript-debug/TurnEndDivider.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index cffc6e9..b82df1f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -288,26 +288,32 @@ watch(() => route.name, (newPage) => { - +
+ + + + +
{{ formatDuration(conversation.metadata.startTime, conversation.metadata.endTime) }} @@ -601,6 +612,26 @@ function formatDuration(start: string, end: string): string { flex-shrink: 0; } +.new-session-status-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border: none; + background: transparent; + border-radius: 3px; + cursor: pointer; + color: var(--text-muted); + flex-shrink: 0; + transition: all 0.15s; +} + +.new-session-status-btn:hover { + background: var(--bg-hover); + color: var(--accent, #6366f1); +} + .status-id { font-size: 9px; font-weight: 600; diff --git a/frontend/src/components/transcript-debug/TurnEndDivider.vue b/frontend/src/components/transcript-debug/TurnEndDivider.vue new file mode 100644 index 0000000..b468cdd --- /dev/null +++ b/frontend/src/components/transcript-debug/TurnEndDivider.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/frontend/src/components/transcript-debug/UserInput.vue b/frontend/src/components/transcript-debug/UserInput.vue index 3984410..d185c17 100644 --- a/frontend/src/components/transcript-debug/UserInput.vue +++ b/frontend/src/components/transcript-debug/UserInput.vue @@ -81,6 +81,7 @@ watch(() => props.voiceTranscript, (newText) => { class="input-field" :style="{ maxHeight: maxH }" :placeholder="notReady ? 'Starting terminal...' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'" + rows="1" :disabled="processing || notReady" @keydown="handleKeydown" /> @@ -181,7 +182,7 @@ watch(() => props.voiceTranscript, (newText) => { field-sizing: content; min-height: 1lh; overflow-y: auto; - padding: 0 0.25rem; + padding: 0.15rem 0.25rem; font-family: inherit; } diff --git a/frontend/src/components/transcript-debug/index.ts b/frontend/src/components/transcript-debug/index.ts index 34bffe4..d43f000 100644 --- a/frontend/src/components/transcript-debug/index.ts +++ b/frontend/src/components/transcript-debug/index.ts @@ -8,6 +8,7 @@ export { default as ToolCallBlock } from './ToolCallBlock.vue' export { default as ToolResultBlock } from './ToolResultBlock.vue' export { default as ProgressEvent } from './ProgressEvent.vue' export { default as SystemMessage } from './SystemMessage.vue' +export { default as TurnEndDivider } from './TurnEndDivider.vue' export { default as UserInput } from './UserInput.vue' export { default as PermissionApproval } from './PermissionApproval.vue' export { default as PlanApproval } from './PlanApproval.vue' @@ -15,4 +16,5 @@ export { default as CodeBlock } from './CodeBlock.vue' export { default as AgentBadge } from './AgentBadge.vue' export { default as ResumeTerminalButton } from './ResumeTerminalButton.vue' export { default as VoiceMicButton } from './VoiceMicButton.vue' +export { default as NewSessionModal } from './NewSessionModal.vue' export { AquaticBackground } from './aquaticBackground' diff --git a/frontend/src/composables/transcript-debug/useTranscriptDebug.ts b/frontend/src/composables/transcript-debug/useTranscriptDebug.ts index 4a46556..b846685 100644 --- a/frontend/src/composables/transcript-debug/useTranscriptDebug.ts +++ b/frontend/src/composables/transcript-debug/useTranscriptDebug.ts @@ -113,6 +113,7 @@ export function useTranscriptDebug() { ) const awaitingNewSession = ref(false) + const pendingPrompt = ref(null) // ── Server registry HTTP helpers ── @@ -324,7 +325,7 @@ export function useTranscriptDebug() { activeTerminalSessionId.value = null } - async function createNewSession() { + async function createNewSession(initialPrompt?: string) { parkCurrentTerminal() selectedSessionId.value = null @@ -333,6 +334,7 @@ export function useTranscriptDebug() { error.value = null processing.value = false optimisticMessage.value = null + pendingPrompt.value = initialPrompt?.trim() || null awaitingNewSession.value = true startTerminal() // no sessionId → brand new session @@ -421,6 +423,14 @@ export function useTranscriptDebug() { selectedSessionId.value = changedSessionId saveState() await fetchSessionContent(changedSessionId) + + // Auto-send queued initial prompt + if (pendingPrompt.value) { + const prompt = pendingPrompt.value + pendingPrompt.value = null + // Small delay to let terminal fully settle + setTimeout(() => sendPrompt(prompt), 300) + } return } @@ -900,20 +910,42 @@ export function useTranscriptDebug() { if (entry.type === 'system') { const se = entry as any - const content = se.message?.content - const textContent = typeof content === 'string' - ? content - : Array.isArray(content) - ? content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('\n') - : JSON.stringify(se.data || se.message || '') + const isTurnEnd = se.subtype === 'stop_hook_summary' || se.subtype === 'turn_duration' - messages.push({ - kind: 'system', - uuid: se.uuid || crypto.randomUUID(), - timestamp: se.timestamp || '', - content: textContent, - subtype: se.subtype - } as ParsedSystemMessage) + if (isTurnEnd) { + // Merge consecutive turn-end messages into one divider + const prev = messages[messages.length - 1] + if (prev?.kind === 'system' && (prev as ParsedSystemMessage).subtype === 'turn_end') { + // Merge into existing turn_end + if (se.subtype === 'turn_duration' && se.durationMs != null) { + (prev as ParsedSystemMessage).durationMs = se.durationMs + } + } else { + messages.push({ + kind: 'system', + uuid: se.uuid || crypto.randomUUID(), + timestamp: se.timestamp || '', + content: '', + subtype: 'turn_end', + durationMs: se.subtype === 'turn_duration' ? se.durationMs : undefined + } as ParsedSystemMessage) + } + } else { + const content = se.message?.content + const textContent = typeof content === 'string' + ? content + : Array.isArray(content) + ? content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('\n') + : JSON.stringify(se.data || se.message || '') + + messages.push({ + kind: 'system', + uuid: se.uuid || crypto.randomUUID(), + timestamp: se.timestamp || '', + content: textContent, + subtype: se.subtype + } as ParsedSystemMessage) + } } } diff --git a/frontend/src/types/transcript-debug.ts b/frontend/src/types/transcript-debug.ts index 2cc5d85..71cb8c6 100644 --- a/frontend/src/types/transcript-debug.ts +++ b/frontend/src/types/transcript-debug.ts @@ -214,6 +214,7 @@ export interface ParsedSystemMessage { timestamp: string content: string subtype?: string + durationMs?: number } // ── Terminal slot (persistent terminal registry) ──