fix: clients sync to server terminals instead of creating new ones
- Remove auto-creation of terminal sessions from init/selectSession/switchAgent - Clients only connect to existing alive terminals from server registry - Remove localStorage persistence (agent/sessionId) — state derived from server - Refine session-state types: new AgentStatus values, LastError interface - UI improvements: AgentBadge, ChatContainer, UserInput, BashCard updates - Simplify claude-hook routes, update session-state service
This commit is contained in:
@@ -1,14 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import type { TerminalSlot } from '@/types/transcript-debug'
|
||||
import { useSessionState, type AgentStatus } from '@/stores/session-state'
|
||||
|
||||
const props = defineProps<{
|
||||
agent: string
|
||||
connected: boolean
|
||||
terminals: TerminalSlot[]
|
||||
activeSessionId: string | null
|
||||
model?: string
|
||||
version?: string
|
||||
}>()
|
||||
|
||||
const sessionStore = useSessionState()
|
||||
|
||||
const STATUS_COLORS: Record<AgentStatus, string> = {
|
||||
idle: '#6b7280',
|
||||
thinking: '#60a5fa',
|
||||
reading: '#22d3ee',
|
||||
writing: '#4ade80',
|
||||
toolUse: '#fbbf24',
|
||||
permissionRequest: '#fb923c',
|
||||
interrupted: '#f87171',
|
||||
error: '#f87171',
|
||||
sessionStart: '#60a5fa',
|
||||
sessionEnd: '#6b7280',
|
||||
}
|
||||
|
||||
const agentStatusColor = computed(() => {
|
||||
const state = sessionStore.agents[props.agent]
|
||||
if (!state) return null
|
||||
return STATUS_COLORS[state.status] || '#6b7280'
|
||||
})
|
||||
|
||||
const agentStatusClass = computed(() => {
|
||||
const state = sessionStore.agents[props.agent]
|
||||
return state?.status || 'idle'
|
||||
})
|
||||
|
||||
const activeIndex = computed(() => {
|
||||
if (!props.activeSessionId) return -1
|
||||
return props.terminals.findIndex(t => t.sessionId === props.activeSessionId)
|
||||
@@ -60,7 +89,8 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
|
||||
<template>
|
||||
<div ref="wrapperRef" class="agent-badge-wrapper" :class="{ connected }" @click="toggle">
|
||||
<span v-if="activeSlot" class="state-dot badge-dot" :style="{ background: slotColor(activeSlot) }" />
|
||||
<span v-if="agentStatusColor" class="state-dot status-dot" :class="agentStatusClass" :style="{ background: agentStatusColor }" />
|
||||
<span v-else-if="activeSlot" class="state-dot badge-dot" :style="{ background: slotColor(activeSlot) }" />
|
||||
<span class="agent-label">{{ agent }}</span>
|
||||
<span v-if="terminals.length" class="term-count">{{ activeIndex >= 0 ? `${activeIndex + 1}/${terminals.length}` : terminals.length }}</span>
|
||||
<svg class="caret" :class="{ open: isOpen }" width="6" height="6" viewBox="0 0 6 6" shape-rendering="crispEdges">
|
||||
@@ -70,6 +100,10 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
</svg>
|
||||
<Transition name="dropdown">
|
||||
<div v-if="isOpen" class="dropdown">
|
||||
<div v-if="model || version" class="dropdown-meta">
|
||||
<span v-if="model" class="meta-model">{{ model }}</span>
|
||||
<span v-if="version" class="meta-version">v{{ version }}</span>
|
||||
</div>
|
||||
<div v-if="terminals.length === 0" class="dropdown-item empty">No terminals</div>
|
||||
<div
|
||||
v-for="(t, idx) in terminals"
|
||||
@@ -136,12 +170,22 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
.badge-dot,
|
||||
.status-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.thinking {
|
||||
animation: pulse-badge 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-badge {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.term-count {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
@@ -188,6 +232,30 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.dropdown-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px 3px;
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.meta-model {
|
||||
font-size: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: rgba(165, 180, 252, 0.7);
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
padding: 0 4px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.meta-version {
|
||||
font-size: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 5px 10px;
|
||||
font-size: 9px;
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SectionSummary
|
||||
} from '@/types/transcript-debug'
|
||||
import type { EphemeralTerminal } from '@/composables/useEphemeralTerminal'
|
||||
import { useSessionState, type AgentStatus } from '@/stores/session-state'
|
||||
import UserMessageBubble from './UserMessageBubble.vue'
|
||||
import AssistantMessageBubble from './AssistantMessageBubble.vue'
|
||||
import ProgressEvent from './ProgressEvent.vue'
|
||||
@@ -21,7 +22,7 @@ import ResumeTerminalButton from './ResumeTerminalButton.vue'
|
||||
const props = defineProps<{
|
||||
conversation: ParsedConversation
|
||||
processing?: boolean
|
||||
terminalReady?: boolean
|
||||
terminalReady?: boolean | null
|
||||
terminal?: EphemeralTerminal | null
|
||||
showSelector?: boolean
|
||||
agents?: { id: AgentName; label: string }[]
|
||||
@@ -44,6 +45,36 @@ const props = defineProps<{
|
||||
hookPermissionMode?: string
|
||||
}>()
|
||||
|
||||
// ── Agent status display ──
|
||||
const sessionStore = useSessionState()
|
||||
|
||||
const STATUS_DISPLAY: Record<AgentStatus, { color: string; label: string }> = {
|
||||
idle: { color: '#6b7280', label: 'Available' },
|
||||
thinking: { color: '#60a5fa', label: 'Thinking...' },
|
||||
reading: { color: '#22d3ee', label: 'Reading' },
|
||||
writing: { color: '#4ade80', label: 'Writing' },
|
||||
toolUse: { color: '#fbbf24', label: 'Using' },
|
||||
permissionRequest: { color: '#fb923c', label: 'Waiting approval' },
|
||||
interrupted: { color: '#f87171', label: 'Interrupted' },
|
||||
error: { color: '#f87171', label: 'Error' },
|
||||
sessionStart: { color: '#60a5fa', label: 'Starting' },
|
||||
sessionEnd: { color: '#6b7280', label: 'Session ended' },
|
||||
}
|
||||
|
||||
const agentStatus = computed(() => {
|
||||
const agent = props.selectedAgent
|
||||
if (!agent) return null
|
||||
const state = sessionStore.agents[agent]
|
||||
if (!state) return null
|
||||
const display = STATUS_DISPLAY[state.status] || STATUS_DISPLAY.idle
|
||||
const toolSuffix = state.currentTool ? ` ${state.currentTool.name}` : ''
|
||||
return {
|
||||
...display,
|
||||
label: display.label + toolSuffix,
|
||||
status: state.status,
|
||||
}
|
||||
})
|
||||
|
||||
// ── Derived display values ──
|
||||
const permissionMode = computed(() => props.hookPermissionMode || '')
|
||||
const fullCwd = computed(() => props.conversation.metadata.cwd || '')
|
||||
@@ -629,8 +660,12 @@ function formatDuration(start: string, end: string): string {
|
||||
/>
|
||||
|
||||
<div class="status-bar">
|
||||
<span v-if="agentStatus" class="agent-status-indicator" :title="agentStatus.label">
|
||||
<span class="agent-status-dot" :class="agentStatus.status" :style="{ background: agentStatus.color }" />
|
||||
<span class="agent-status-label">{{ agentStatus.label }}</span>
|
||||
</span>
|
||||
<span v-if="permissionMode" class="meta-badge mode">{{ permissionMode }}</span>
|
||||
<span v-if="displayCwd" class="meta-badge origin" :title="fullCwd">{{ displayCwd }}</span>
|
||||
<span v-if="displayCwd" class="meta-badge origin" tabindex="0">{{ fullCwd }}</span>
|
||||
<span class="meta-count">{{ conversation.messages.length }} msgs</span>
|
||||
<button class="new-session-status-btn" @click="emit('createSession')" title="New session">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
@@ -888,9 +923,21 @@ function formatDuration(start: string, end: string): string {
|
||||
.meta-badge.origin {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: var(--accent, #6366f1);
|
||||
max-width: 120px;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
transition: max-width 0.35s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.meta-badge.origin:hover,
|
||||
.meta-badge.origin:focus {
|
||||
max-width: 400px;
|
||||
direction: ltr;
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
}
|
||||
|
||||
.meta-duration {
|
||||
@@ -1300,4 +1347,40 @@ 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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@ import VoiceMicButton from './VoiceMicButton.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
processing?: boolean
|
||||
terminalReady?: boolean
|
||||
terminalReady?: boolean | null // null = no terminal, false = starting, true = ready
|
||||
voiceTranscript?: string
|
||||
isRecording?: boolean
|
||||
voiceMode?: 'web' | 'whisper'
|
||||
@@ -25,12 +25,15 @@ const emit = defineEmits<{
|
||||
|
||||
const input = ref('')
|
||||
|
||||
const notReady = computed(() => props.terminalReady === false)
|
||||
const isDisabled = computed(() => !input.value.trim() || props.processing || notReady.value)
|
||||
// terminalReady: null = no terminal (show "no terminal"), false = starting, true = ready
|
||||
const terminalStarting = computed(() => props.terminalReady === false) // explicitly false, not null
|
||||
const noTerminal = computed(() => props.terminalReady === null)
|
||||
const canSend = computed(() => props.terminalReady === true && !props.processing)
|
||||
const isDisabled = computed(() => !input.value.trim() || !canSend.value)
|
||||
|
||||
function handleSend() {
|
||||
const msg = input.value.trim()
|
||||
if (!msg || props.processing || notReady.value) return
|
||||
if (!msg || !canSend.value) return
|
||||
emit('send', msg)
|
||||
input.value = ''
|
||||
}
|
||||
@@ -63,7 +66,7 @@ watch(() => props.voiceTranscript, (newText) => {
|
||||
|
||||
<template>
|
||||
<div class="user-input">
|
||||
<div v-if="notReady" class="processing-bar starting">
|
||||
<div v-if="terminalStarting" class="processing-bar starting">
|
||||
<span class="processing-dots">
|
||||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
||||
</span>
|
||||
@@ -75,14 +78,14 @@ watch(() => props.voiceTranscript, (newText) => {
|
||||
</span>
|
||||
<span>Agent is processing...</span>
|
||||
</div>
|
||||
<div class="input-container" :class="{ disabled: processing || notReady }">
|
||||
<div class="input-container" :class="{ disabled: !canSend }">
|
||||
<textarea
|
||||
v-model="input"
|
||||
class="input-field"
|
||||
:style="{ maxHeight: maxH }"
|
||||
:placeholder="notReady ? 'Starting terminal...' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
|
||||
:placeholder="terminalStarting ? 'Starting terminal...' : noTerminal ? 'No terminal — use + to create session' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
|
||||
rows="1"
|
||||
:disabled="processing || notReady"
|
||||
:disabled="!canSend"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
<VoiceMicButton
|
||||
@@ -90,7 +93,7 @@ watch(() => props.voiceTranscript, (newText) => {
|
||||
:is-recording="isRecording ?? false"
|
||||
:voice-mode="voiceMode"
|
||||
:whisper-status="whisperStatus ?? 'offline'"
|
||||
:disabled="processing || notReady"
|
||||
:disabled="!canSend"
|
||||
@start="emit('startRecording')"
|
||||
@stop="emit('stopRecording')"
|
||||
/>
|
||||
|
||||
@@ -19,9 +19,14 @@ const highlightedCommand = computed(() => highlightCode(command.value, 'bash'))
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
const descPreview = computed(() => {
|
||||
const d = description.value.trim()
|
||||
return d.length > 50 ? d.slice(0, 50) + '...' : d
|
||||
})
|
||||
|
||||
const cmdPreview = computed(() => {
|
||||
const c = command.value.replace(/\n/g, ' ').trim()
|
||||
return c.length > 60 ? c.slice(0, 60) + '...' : c
|
||||
return c.length > 40 ? c.slice(0, 40) + '...' : c
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -35,6 +40,7 @@ const cmdPreview = computed(() => {
|
||||
</svg>
|
||||
</span>
|
||||
<span class="card-label">Bash</span>
|
||||
<span v-if="description" class="desc-preview" :title="description">{{ descPreview }}</span>
|
||||
<code class="cmd-preview" :title="command">$ {{ cmdPreview }}</code>
|
||||
<span v-if="runInBackground" class="info-badge">bg</span>
|
||||
<span v-if="timeout" class="info-badge">{{ timeout }}ms</span>
|
||||
@@ -90,7 +96,7 @@ const cmdPreview = computed(() => {
|
||||
}
|
||||
.bash-card.error .card-label { color: rgba(239, 68, 68, 0.7); }
|
||||
|
||||
.cmd-preview {
|
||||
.desc-preview {
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: var(--text-secondary);
|
||||
@@ -101,6 +107,17 @@ const cmdPreview = computed(() => {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.cmd-preview {
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.info-badge, .error-badge {
|
||||
font-size: 9px;
|
||||
padding: 0.05rem 0.3rem;
|
||||
|
||||
@@ -27,39 +27,14 @@ import type {
|
||||
export function useTranscriptDebug() {
|
||||
// ── Centralized session state ──
|
||||
const sessionStore = useSessionState()
|
||||
// ── Persistence ──
|
||||
const STORAGE_KEY = 'transcript-debug-selection'
|
||||
|
||||
function restoreState(): { agent: AgentName; sessionId: string | null } | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
const data = JSON.parse(raw)
|
||||
if (data.agent === 'ejecutor' || data.agent === 'nucleo000' || data.agent === 'claude') {
|
||||
return { agent: data.agent, sessionId: data.sessionId || null }
|
||||
}
|
||||
} catch {}
|
||||
return null
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
agent: selectedAgent.value,
|
||||
sessionId: selectedSessionId.value
|
||||
}))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const saved = restoreState()
|
||||
|
||||
const selectedAgent = ref<AgentName>(saved?.agent || 'ejecutor')
|
||||
const selectedAgent = ref<AgentName>('ejecutor')
|
||||
const sessions = ref<SessionInfo[]>([])
|
||||
const selectedSessionId = ref<string | null>(saved?.sessionId || null)
|
||||
const selectedSessionId = ref<string | null>(null)
|
||||
const rawContent = ref<string>('')
|
||||
const conversation = ref<ParsedConversation | null>(null)
|
||||
const loading = ref(false)
|
||||
const transitioning = ref(!!saved?.sessionId)
|
||||
const transitioning = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isRealtime = ref(false)
|
||||
|
||||
@@ -92,7 +67,11 @@ export function useTranscriptDebug() {
|
||||
return localTerminals.get(activeTerminalSessionId.value) ?? null
|
||||
})
|
||||
|
||||
const terminalReady = computed(() => ephemeral.value?.state.value === 'running')
|
||||
// null = no terminal exists, false = terminal connecting/starting, true = running
|
||||
const terminalReady = computed<boolean | null>(() => {
|
||||
if (!ephemeral.value) return null
|
||||
return ephemeral.value.state.value === 'running'
|
||||
})
|
||||
|
||||
// Computed: terminal list for AgentBadge dropdown (from server registry)
|
||||
const openTerminals = computed<TerminalSlot[]>(() =>
|
||||
@@ -276,7 +255,6 @@ export function useTranscriptDebug() {
|
||||
|
||||
// Load the target session's transcript
|
||||
selectedSessionId.value = transcriptSessionId
|
||||
saveState()
|
||||
await fetchSessionContent(transcriptSessionId)
|
||||
|
||||
// Connect to the terminal
|
||||
@@ -386,7 +364,6 @@ export function useTranscriptDebug() {
|
||||
}
|
||||
|
||||
selectedSessionId.value = changedSessionId
|
||||
saveState()
|
||||
await fetchSessionContent(changedSessionId)
|
||||
|
||||
// Auto-send queued initial prompt
|
||||
@@ -449,7 +426,7 @@ export function useTranscriptDebug() {
|
||||
// Clear optimistic processing if server state is now idle
|
||||
if (optimisticProcessing.value) {
|
||||
const agentState = sessionStore.agents[selectedAgent.value]
|
||||
if (agentState && (agentState.status === 'idle' || agentState.status === 'sessionStart')) {
|
||||
if (agentState && ['idle', 'sessionStart', 'sessionEnd'].includes(agentState.status)) {
|
||||
optimisticProcessing.value = false
|
||||
}
|
||||
}
|
||||
@@ -498,13 +475,17 @@ export function useTranscriptDebug() {
|
||||
if (optimisticProcessing.value) return true
|
||||
const agentState = sessionStore.agents[selectedAgent.value]
|
||||
if (!agentState) return false
|
||||
return agentState.status !== 'idle' && agentState.status !== 'sessionStart'
|
||||
return !['idle', 'sessionStart', 'sessionEnd'].includes(agentState.status)
|
||||
})
|
||||
|
||||
async function sendPrompt(text: string) {
|
||||
if (!text.trim() || !selectedSessionId.value) return
|
||||
|
||||
if (!ephemeral.value || ephemeral.value.state.value !== 'running') {
|
||||
if (!ephemeral.value) {
|
||||
error.value = 'No terminal — use + to create a new session'
|
||||
return
|
||||
}
|
||||
if (ephemeral.value.state.value !== 'running') {
|
||||
error.value = 'Terminal not ready — wait for it to start'
|
||||
return
|
||||
}
|
||||
@@ -557,38 +538,43 @@ export function useTranscriptDebug() {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await fetchSessions()
|
||||
|
||||
let targetSession = selectedSessionId.value
|
||||
|
||||
// Validate saved session still exists
|
||||
if (targetSession && !sessions.value.some(s => s.id === targetSession)) {
|
||||
targetSession = null
|
||||
// Derive initial state from server registry: if there's an alive terminal, sync to it
|
||||
const aliveEntry = serverRegistry.value.find(e => e.alive)
|
||||
if (aliveEntry) {
|
||||
selectedAgent.value = (aliveEntry.agent as AgentName) || 'ejecutor'
|
||||
}
|
||||
|
||||
// Fall back to newest
|
||||
await fetchSessions()
|
||||
|
||||
let targetSession: string | null = null
|
||||
|
||||
if (aliveEntry) {
|
||||
// Sync to the alive terminal's transcript session
|
||||
targetSession = aliveEntry.transcriptSessionId
|
||||
// Validate it exists in sessions list
|
||||
if (targetSession && !sessions.value.some(s => s.id === targetSession)) {
|
||||
targetSession = null
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to newest session
|
||||
if (!targetSession && sessions.value.length > 0) {
|
||||
targetSession = sessions.value[0].id
|
||||
}
|
||||
|
||||
if (targetSession) {
|
||||
selectedSessionId.value = targetSession
|
||||
saveState()
|
||||
await fetchSessionContent(targetSession)
|
||||
|
||||
// Check if there's already a terminal for this session in the registry
|
||||
// Connect to existing terminal if one exists (clients never create terminals — only the server does)
|
||||
const existing = serverRegistry.value.find(
|
||||
e => e.transcriptSessionId === targetSession
|
||||
e => e.transcriptSessionId === targetSession && e.alive
|
||||
)
|
||||
if (existing && existing.alive) {
|
||||
// Connect to existing terminal (may have been created by another client)
|
||||
if (existing) {
|
||||
connectToTerminal(targetSession)
|
||||
} else {
|
||||
startTerminal(targetSession)
|
||||
}
|
||||
} else {
|
||||
selectedSessionId.value = null
|
||||
saveState()
|
||||
}
|
||||
|
||||
transitioning.value = false
|
||||
@@ -617,20 +603,17 @@ export function useTranscriptDebug() {
|
||||
selectedSessionId.value = sessions.value[0].id
|
||||
await fetchSessionContent(sessions.value[0].id)
|
||||
|
||||
// Check if there's already a terminal for this session
|
||||
// Connect to existing terminal if one exists (clients never create terminals)
|
||||
const existing = serverRegistry.value.find(
|
||||
e => e.transcriptSessionId === sessions.value[0].id && e.alive
|
||||
)
|
||||
if (existing) {
|
||||
connectToTerminal(sessions.value[0].id)
|
||||
} else {
|
||||
startTerminal(sessions.value[0].id)
|
||||
}
|
||||
} else {
|
||||
selectedSessionId.value = null
|
||||
}
|
||||
|
||||
saveState()
|
||||
transitioning.value = false
|
||||
loading.value = false
|
||||
}
|
||||
@@ -647,20 +630,17 @@ export function useTranscriptDebug() {
|
||||
await new Promise(r => setTimeout(r, 150))
|
||||
|
||||
selectedSessionId.value = sessionId
|
||||
saveState()
|
||||
await fetchSessionContent(sessionId)
|
||||
|
||||
transitioning.value = false
|
||||
loading.value = false
|
||||
|
||||
// Check if there's already a terminal for this session in the registry
|
||||
// Connect to existing terminal if one exists (clients never create terminals)
|
||||
const existing = serverRegistry.value.find(
|
||||
e => e.transcriptSessionId === sessionId && e.alive
|
||||
)
|
||||
if (existing) {
|
||||
connectToTerminal(sessionId)
|
||||
} else {
|
||||
startTerminal(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,10 @@ export interface EphemeralTerminal {
|
||||
|
||||
export function useEphemeralTerminal(
|
||||
command: string,
|
||||
existingSessionId?: string
|
||||
existingSessionId?: string,
|
||||
options?: {
|
||||
onExtraMessage?: (msg: Record<string, any>) => void
|
||||
}
|
||||
): EphemeralTerminal {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const connected = ref(false)
|
||||
@@ -141,6 +144,9 @@ export function useEphemeralTerminal(
|
||||
case 'error':
|
||||
renderer.writeln(`\x1b[31mError: ${msg.message}\x1b[0m`)
|
||||
break
|
||||
default:
|
||||
options?.onExtraMessage?.(msg)
|
||||
break
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
processing,
|
||||
ephemeral,
|
||||
terminalReady,
|
||||
hookMeta,
|
||||
init,
|
||||
switchAgent,
|
||||
selectSession,
|
||||
@@ -144,6 +145,7 @@ onUnmounted(() => {
|
||||
:voice-transcript="voiceTranscript + voiceInterim"
|
||||
:last-audio-url="lastAudioUrl"
|
||||
:is-playing-audio="isPlayingAudio"
|
||||
:hook-permission-mode="hookMeta.permissionMode"
|
||||
@send="handleSend"
|
||||
@create-session="handleCreateSession"
|
||||
@start-recording="voice.startRecording()"
|
||||
|
||||
@@ -5,14 +5,15 @@ import { ref, computed } from 'vue'
|
||||
|
||||
export type AgentStatus =
|
||||
| 'idle'
|
||||
| 'processing'
|
||||
| 'thinking'
|
||||
| 'reading'
|
||||
| 'writing'
|
||||
| 'toolUse'
|
||||
| 'toolDone'
|
||||
| 'permissionRequest'
|
||||
| 'notification'
|
||||
| 'interrupted'
|
||||
| 'error'
|
||||
| 'sessionStart'
|
||||
| 'sessionEnd'
|
||||
|
||||
export interface ActiveTool {
|
||||
name: string
|
||||
@@ -48,6 +49,13 @@ export interface AgentTerminalInfo {
|
||||
connectedClients: number
|
||||
}
|
||||
|
||||
export interface LastError {
|
||||
tool: string
|
||||
message: string
|
||||
interrupted: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface AgentSessionState {
|
||||
agent: string
|
||||
sessionId: string | null
|
||||
@@ -59,6 +67,7 @@ export interface AgentSessionState {
|
||||
currentTool: ActiveTool | null
|
||||
lastActivity: number
|
||||
lastStopResponse: string | null
|
||||
lastError: LastError | null
|
||||
pendingApprovals: PendingApproval[]
|
||||
terminal: AgentTerminalInfo
|
||||
notifications: SessionNotification[]
|
||||
@@ -130,6 +139,14 @@ export const useSessionState = defineStore('session-state', () => {
|
||||
)
|
||||
)
|
||||
|
||||
const isProcessing = computed(() =>
|
||||
agentList.value.some(a => !['idle', 'sessionStart', 'sessionEnd'].includes(a.status))
|
||||
)
|
||||
|
||||
const hasErrors = computed(() =>
|
||||
agentList.value.some(a => a.status === 'error' || a.status === 'interrupted')
|
||||
)
|
||||
|
||||
const visibleNotifications = computed(() => {
|
||||
const now = Date.now()
|
||||
return agentList.value
|
||||
@@ -199,6 +216,8 @@ export const useSessionState = defineStore('session-state', () => {
|
||||
anyPendingApprovals,
|
||||
totalPendingApprovals,
|
||||
allPendingApprovals,
|
||||
isProcessing,
|
||||
hasErrors,
|
||||
visibleNotifications,
|
||||
handleMessage,
|
||||
respondApproval,
|
||||
|
||||
Reference in New Issue
Block a user