diff --git a/frontend/src/components/FloatingTranscriptDebug.vue b/frontend/src/components/FloatingTranscriptDebug.vue index 06a4324..1a88c7f 100644 --- a/frontend/src/components/FloatingTranscriptDebug.vue +++ b/frontend/src/components/FloatingTranscriptDebug.vue @@ -970,29 +970,15 @@ onBeforeUnmount(() => { pointer-events: none !important; } -/* Idle: slide user-input down and fade out (only if empty) */ -.aero-win:not(.chrome-visible) .content :deep(.user-input) { +/* Idle: slide bottom-overlay down and fade out */ +.aero-win:not(.chrome-visible) .content :deep(.bottom-overlay) { opacity: 0 !important; transform: translateY(100%) !important; pointer-events: none !important; } -/* Keep user-input visible when textarea has text */ -.aero-win:not(.chrome-visible) .content :deep(.user-input:has(.input-field:not(:placeholder-shown))) { - opacity: 1 !important; - transform: none !important; - pointer-events: auto !important; -} - -/* Idle: hide status bar */ -.aero-win:not(.chrome-visible) .content :deep(.status-bar) { - opacity: 0 !important; - transform: translateY(100%) !important; - pointer-events: none !important; -} - -/* Keep status-bar visible when input has text */ -.aero-win:not(.chrome-visible) .content :deep(.status-bar:has(~ .user-input .input-field:not(:placeholder-shown))) { +/* Keep bottom-overlay visible when textarea has text */ +.aero-win:not(.chrome-visible) .content :deep(.bottom-overlay:has(.input-field:not(:placeholder-shown))) { opacity: 1 !important; transform: none !important; pointer-events: auto !important; @@ -1022,11 +1008,7 @@ onBeforeUnmount(() => { transition: opacity 0.35s ease, transform 0.35s ease !important; } -.content :deep(.user-input) { - transition: opacity 0.35s ease, transform 0.35s ease !important; -} - -.content :deep(.status-bar) { +.content :deep(.bottom-overlay) { transition: opacity 0.35s ease, transform 0.35s ease !important; } @@ -1321,16 +1303,23 @@ onBeforeUnmount(() => { } -/* Status bar: absolute overlay at very bottom */ -.content :deep(.status-bar) { +/* Bottom overlay: absolute container for lifecycle + input + status */ +.content :deep(.bottom-overlay) { position: absolute !important; bottom: 0 !important; left: 0 !important; right: 0 !important; - z-index: 4 !important; - background: rgba(0, 6, 18, 0.6) !important; + z-index: 3 !important; + display: flex !important; + flex-direction: column !important; + background: rgba(0, 6, 18, 0.5) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; + border-top: 1px solid rgba(255, 255, 255, 0.06) !important; +} + +.content :deep(.status-bar) { + background: transparent !important; border-top: 1px solid rgba(255, 255, 255, 0.04) !important; border-bottom: none !important; padding: 0.15rem 0.5rem !important; @@ -1368,20 +1357,19 @@ onBeforeUnmount(() => { color: rgba(255,255,255,0.2) !important; } -/* UserInput: absolute overlay above status bar */ +/* UserInput: inside bottom-overlay, no absolute positioning needed */ .content :deep(.user-input) { - position: absolute !important; - bottom: 20px !important; - left: 0 !important; - right: 0 !important; - z-index: 3 !important; - background: rgba(0, 6, 18, 0.5) !important; - backdrop-filter: blur(8px) !important; - -webkit-backdrop-filter: blur(8px) !important; - border-top: 1px solid rgba(255, 255, 255, 0.06) !important; + background: transparent !important; + border-top: 1px solid rgba(255, 255, 255, 0.04) !important; padding: 0.3rem 0.5rem !important; } +/* Lifecycle ribbon: inside bottom-overlay, flows naturally above user-input */ +.content :deep(.lifecycle-ribbon) { + background: transparent !important; + pointer-events: none !important; +} + /* Dark overlay on input-container so text is readable */ .content :deep(.input-container) { background: rgba(0, 6, 18, 0.8) !important; diff --git a/frontend/src/components/transcript-debug/ChatContainer.vue b/frontend/src/components/transcript-debug/ChatContainer.vue index 37f9416..7879a83 100644 --- a/frontend/src/components/transcript-debug/ChatContainer.vue +++ b/frontend/src/components/transcript-debug/ChatContainer.vue @@ -17,6 +17,7 @@ import ProgressEvent from './ProgressEvent.vue' import SystemMessage from './SystemMessage.vue' import TurnEndDivider from './TurnEndDivider.vue' import UserInput from './UserInput.vue' +import SessionLifecycleStatus from './SessionLifecycleStatus.vue' import ResumeTerminalButton from './ResumeTerminalButton.vue' const props = defineProps<{ @@ -75,6 +76,19 @@ const agentStatus = computed(() => { } }) +// ── Lifecycle hook event (for SessionLifecycleStatus) ── +const lifecycleEvent = computed(() => { + const agent = props.selectedAgent + if (!agent) return null + return sessionStore.agents[agent]?.lastHookEvent ?? null +}) + +const lifecycleDetail = computed(() => { + const agent = props.selectedAgent + if (!agent) return '' + return sessionStore.agents[agent]?.lastHookDetail ?? '' +}) + // ── Derived display values ── const permissionMode = computed(() => props.hookPermissionMode || '') const fullCwd = computed(() => props.conversation.metadata.cwd || '') @@ -646,24 +660,26 @@ function formatDuration(start: string, end: string): string { - +
+ -
- - - {{ agentStatus.label }} - + + +
{{ permissionMode }} {{ fullCwd }} {{ conversation.messages.length }} msgs @@ -707,6 +723,7 @@ function formatDuration(start: string, end: string): string {
+
@@ -791,6 +808,13 @@ function formatDuration(start: string, end: string): string { color: #22c55e; } +/* ── Bottom overlay (lifecycle + input + status bar) ── */ +.bottom-overlay { + display: flex; + flex-direction: column; + flex-shrink: 0; +} + /* ── Status bar (below input) ── */ .status-bar { display: flex; @@ -1347,40 +1371,5 @@ function formatDuration(start: string, end: string): string { flex-shrink: 0; } -/* ── Agent status indicator ── */ -.agent-status-indicator { - display: inline-flex; - align-items: center; - gap: 4px; - flex-shrink: 0; -} - -.agent-status-dot { - width: 5px; - height: 5px; - flex-shrink: 0; - border-radius: 0; - transition: background 0.2s; -} - -.agent-status-dot.thinking { - animation: pulse-status 1.5s ease-in-out infinite; -} - -@keyframes pulse-status { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } -} - -.agent-status-label { - font-size: 9px; - font-weight: 600; - font-family: 'Courier New', monospace; - color: var(--text-muted, rgba(255,255,255,0.4)); - white-space: nowrap; - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/frontend/src/components/transcript-debug/SessionLifecycleStatus.vue b/frontend/src/components/transcript-debug/SessionLifecycleStatus.vue new file mode 100644 index 0000000..5801988 --- /dev/null +++ b/frontend/src/components/transcript-debug/SessionLifecycleStatus.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/frontend/src/stores/session-state.ts b/frontend/src/stores/session-state.ts index 2ac4924..90d7467 100644 --- a/frontend/src/stores/session-state.ts +++ b/frontend/src/stores/session-state.ts @@ -71,6 +71,8 @@ export interface AgentSessionState { pendingApprovals: PendingApproval[] terminal: AgentTerminalInfo notifications: SessionNotification[] + lastHookEvent: string | null + lastHookDetail: string | null } // WS message types