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,213 @@
<script setup lang="ts">
import type { HooksApprovalPermissionRequest } from '@/types/hooks-approval'
const props = defineProps<{
request: HooksApprovalPermissionRequest
}>()
const emit = defineEmits<{
respond: [requestId: string, decision: 'allow' | 'deny']
}>()
function formatInput(input: unknown): string {
if (!input) return ''
if (typeof input === 'string') return input
try {
return JSON.stringify(input, null, 2)
} catch {
return String(input)
}
}
function elapsed(): string {
const ms = Date.now() - props.request.timestamp
const s = Math.floor(ms / 1000)
if (s < 60) return `${s}s ago`
return `${Math.floor(s / 60)}m ${s % 60}s ago`
}
</script>
<template>
<div class="permission-card">
<div class="card-header">
<span class="card-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</span>
<span class="card-label">Permission Request</span>
<span class="card-elapsed">{{ elapsed() }}</span>
</div>
<div class="card-body">
<div class="info-row" v-if="request.tool_name">
<span class="info-label">Tool</span>
<code class="info-value tool-name">{{ request.tool_name }}</code>
</div>
<div class="info-row" v-if="request.agent_name">
<span class="info-label">Agent</span>
<span class="info-value">{{ request.agent_name }}</span>
</div>
<div v-if="request.tool_input" class="input-block">
<span class="info-label">Input</span>
<pre class="input-pre">{{ formatInput(request.tool_input) }}</pre>
</div>
</div>
<div class="card-actions">
<button class="btn btn-allow" @click="emit('respond', request.requestId, 'allow')">
<svg 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>
Allow
</button>
<button class="btn btn-deny" @click="emit('respond', request.requestId, 'deny')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
Deny
</button>
</div>
</div>
</template>
<style scoped>
.permission-card {
border: 1px solid var(--border-color);
border-left: 3px solid #f59e0b;
border-radius: 8px;
background: var(--bg-secondary);
overflow: hidden;
animation: slideIn 0.2s ease-out;
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(245, 158, 11, 0.06);
border-bottom: 1px solid var(--border-color);
}
.card-icon {
color: #f59e0b;
display: flex;
align-items: center;
}
.card-label {
font-size: 12px;
font-weight: 600;
color: #f59e0b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.card-elapsed {
margin-left: auto;
font-size: 11px;
color: var(--text-muted);
font-family: 'SF Mono', 'Fira Code', monospace;
}
.card-body {
padding: 0.6rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.info-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.info-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 500;
min-width: 40px;
flex-shrink: 0;
}
.info-value {
font-size: 13px;
color: var(--text-primary);
}
.tool-name {
background: rgba(99, 102, 241, 0.1);
color: var(--accent, #6366f1);
padding: 0.1rem 0.4rem;
border-radius: 4px;
font-size: 12px;
font-family: 'SF Mono', 'Fira Code', monospace;
}
.input-block {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.input-pre {
margin: 0;
padding: 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 11px;
font-family: 'SF Mono', 'Fira Code', monospace;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
max-height: 150px;
overflow-y: auto;
}
.card-actions {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--border-color);
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.15s;
}
.btn-allow {
background: #22c55e;
color: white;
}
.btn-allow:hover {
background: #16a34a;
}
.btn-deny {
background: #ef4444;
color: white;
}
.btn-deny:hover {
background: #dc2626;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>