feat: Add transcript-debug page with multi-agent support, hooks approval, and message selection

- Transcript debug: JSONL viewer, parsed chat view, realtime WebSocket updates, session selector
- Multi-agent: ejecutor, nucleo000, and claude (global ~/.claude/projects/) with agent switcher
- Hooks approval: permission/plan request forwarding via PowerShell hooks, long-poll API, UI modals
- Chat features: session ID copy, select mode with checkboxes, multi-select copy, select all/deselect all
- File watchers for all agent transcript directories with polling fallback on Windows
This commit is contained in:
2026-02-18 23:55:09 -06:00
parent d0fdd04132
commit 9bd6123f97
37 changed files with 5663 additions and 30 deletions

View File

@@ -0,0 +1,344 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useTranscriptDebug } from '@/composables/transcript-debug'
import { SessionSelector, RawJsonViewer, ChatContainer } from '@/components/transcript-debug'
import type { AgentName } from '@/types/transcript-debug'
const {
selectedAgent,
sessions,
selectedSessionId,
rawContent,
conversation,
loading,
transitioning,
error,
lineCount,
isRealtime,
sending,
processing,
init,
switchAgent,
selectSession,
disconnectRealtime,
sendPrompt
} = useTranscriptDebug()
const agents: { id: AgentName; label: string }[] = [
{ id: 'ejecutor', label: 'Ejecutor' },
{ id: 'nucleo000', label: 'nucleo000' },
{ id: 'claude', label: 'Claude' }
]
function handleSend(message: string) {
sendPrompt(message)
}
onMounted(() => {
init()
})
onUnmounted(() => {
disconnectRealtime()
})
</script>
<template>
<div class="transcript-debug-page">
<!-- Header -->
<header class="page-header">
<div class="header-left">
<h2>Transcript Debug</h2>
<span :class="['realtime-dot', { connected: isRealtime }]" :title="isRealtime ? 'Realtime: connected' : 'Realtime: disconnected'">
<svg width="8" height="8" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="currentColor"/></svg>
</span>
</div>
<div class="header-selectors">
<!-- Agent selector -->
<div class="agent-selector">
<button
v-for="a in agents"
:key="a.id"
:class="['agent-btn', { active: selectedAgent === a.id }]"
@click="switchAgent(a.id)"
>
{{ a.label }}
</button>
</div>
<!-- Session selector -->
<SessionSelector
:sessions="sessions"
:selected-id="selectedSessionId"
:loading="loading"
@select="selectSession"
/>
</div>
</header>
<!-- Error -->
<div v-if="error" class="error-bar">{{ error }}</div>
<!-- Content -->
<div class="content-area">
<div v-if="!selectedSessionId" :class="['empty-state', { fading: transitioning }]">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<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" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<p>Select a transcript session to begin</p>
<span>{{ sessions.length }} sessions available</span>
</div>
<div v-else class="split-panels">
<!-- Left: Raw JSONL -->
<div :class="['panel-left', { fading: transitioning }]">
<RawJsonViewer :content="rawContent" />
</div>
<!-- Resize handle -->
<div class="resize-handle"></div>
<!-- Right: Chat -->
<div :class="['panel-right', { fading: transitioning }]">
<ChatContainer
v-if="conversation"
:conversation="conversation"
:processing="processing"
@send="handleSend"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.transcript-debug-page {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
background: var(--bg-primary);
color: var(--text-primary);
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
gap: 1rem;
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.header-left h2 {
font-size: 15px;
font-weight: 600;
margin: 0;
white-space: nowrap;
}
.realtime-dot {
color: var(--text-muted);
opacity: 0.4;
transition: all 0.3s;
}
.realtime-dot.connected {
color: #22c55e;
opacity: 1;
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0%, 100% { filter: drop-shadow(0 0 2px currentColor); }
50% { filter: drop-shadow(0 0 6px currentColor); }
}
.header-selectors {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
/* Agent selector */
.agent-selector {
display: flex;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
}
.agent-btn {
padding: 0.35rem 0.75rem;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.agent-btn:not(:last-child) {
border-right: 1px solid var(--border-color);
}
.agent-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.agent-btn.active {
background: var(--accent);
color: white;
}
.error-bar {
padding: 0.5rem 1rem;
background: rgba(239, 68, 68, 0.1);
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
font-size: 13px;
flex-shrink: 0;
}
.content-area {
display: flex;
flex: 1;
overflow: hidden;
}
.fading {
opacity: 0 !important;
}
.empty-state {
transition: opacity 0.15s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 0.75rem;
color: var(--text-muted);
}
.empty-state svg {
opacity: 0.4;
}
.empty-state p {
font-size: 15px;
color: var(--text-secondary);
margin: 0;
}
.empty-state span {
font-size: 13px;
}
.split-panels {
display: flex;
flex: 1;
overflow: hidden;
}
.panel-left {
width: 35%;
min-width: 250px;
display: flex;
flex-direction: column;
padding: 0.5rem;
padding-right: 0;
transition: opacity 0.15s ease;
}
.resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
flex-shrink: 0;
margin: 0.5rem 2px;
border-radius: 2px;
transition: background 0.15s;
}
.resize-handle:hover {
background: var(--accent);
}
.panel-right {
flex: 1;
min-width: 300px;
display: flex;
flex-direction: column;
padding: 0.5rem;
padding-left: 0;
transition: opacity 0.15s ease;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
}
.header-left h2 {
font-size: 14px;
}
.header-selectors {
flex-direction: column;
align-items: stretch;
width: 100%;
gap: 0.5rem;
}
.split-panels {
flex-direction: column;
}
.panel-left {
width: 100%;
height: 40%;
min-width: 0;
padding: 0.5rem;
}
.resize-handle {
width: 100%;
height: 4px;
margin: 2px 0.5rem;
cursor: row-resize;
}
.panel-right {
flex: 1;
min-width: 0;
min-height: 0;
padding: 0.5rem;
}
}
</style>