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:
344
frontend/src/pages/TranscriptDebugPage.vue
Normal file
344
frontend/src/pages/TranscriptDebugPage.vue
Normal 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>
|
||||
Reference in New Issue
Block a user