feat: unified hook notifier, agent auto-detection, terminal transition UI

- Add hooks/notify.ps1 as single hook handler for all events
- Refactor settings.local.json to use notify.ps1 instead of inline PS
- Add Notification hook, auto-detect agent from session_id/transcript
- Rename agent 'main' to 'claude' across server routes and terminal
- Add loading overlay and error state for terminal switching transitions
- Add transitionError ref to useTranscriptDebug composable
This commit is contained in:
2026-02-21 04:33:42 -06:00
parent b9eec1013b
commit a56796a1be
8 changed files with 172 additions and 45 deletions

View File

@@ -28,6 +28,8 @@ const {
selectedSessionId,
conversation,
loading,
transitioning,
transitionError,
error,
isRealtime,
processing,
@@ -702,6 +704,22 @@ onBeforeUnmount(() => {
<div class="content">
<AquaticBackground />
<div class="readability-overlay" :style="{ background: `rgba(0, 0, 0, ${overlayOpacity})` }" />
<Transition name="terminal-loading">
<div v-if="transitioning" class="terminal-loading-overlay">
<div class="terminal-loading-spinner" />
</div>
</Transition>
<Transition name="terminal-loading">
<div v-if="transitionError" class="terminal-error-overlay" @click="transitionError = null">
<div class="terminal-error-content">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f87171" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<span class="terminal-error-msg">{{ transitionError }}</span>
<span class="terminal-error-hint">Click to dismiss</span>
</div>
</div>
</Transition>
<ChatContainer
ref="chatRef"
v-if="conversation"
@@ -1180,6 +1198,73 @@ onBeforeUnmount(() => {
opacity: 1;
}
/* ── Terminal switching loading overlay ── */
.terminal-loading-overlay {
position: absolute;
inset: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
}
.terminal-loading-spinner {
width: 28px;
height: 28px;
border: 2.5px solid rgba(255, 255, 255, 0.15);
border-top-color: rgba(255, 255, 255, 0.7);
border-radius: 50%;
animation: tl-spin 0.7s linear infinite;
}
@keyframes tl-spin {
to { transform: rotate(360deg); }
}
.terminal-error-overlay {
position: absolute;
inset: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(6px);
cursor: pointer;
}
.terminal-error-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
max-width: 80%;
padding: 1.2rem 1.5rem;
background: rgba(30, 30, 30, 0.85);
border: 1px solid rgba(248, 113, 113, 0.3);
border-radius: 10px;
}
.terminal-error-msg {
font-size: 12px;
color: #fca5a5;
text-align: center;
line-height: 1.4;
word-break: break-word;
}
.terminal-error-hint {
font-size: 10px;
color: rgba(255, 255, 255, 0.35);
}
.terminal-loading-enter-active { transition: opacity 0.15s ease; }
.terminal-loading-leave-active { transition: opacity 0.25s ease; }
.terminal-loading-enter-from,
.terminal-loading-leave-to { opacity: 0; }
/* Override ChatContainer backgrounds: glass-transparent */
.content :deep(.chat-container) {
background: transparent !important;

View File

@@ -35,6 +35,7 @@ export function useTranscriptDebug() {
const conversation = ref<ParsedConversation | null>(null)
const loading = ref(false)
const transitioning = ref(false)
const transitionError = ref<string | null>(null)
const error = ref<string | null>(null)
const isRealtime = ref(false)
@@ -234,9 +235,15 @@ export function useTranscriptDebug() {
async function switchToTerminal(transcriptSessionId: string) {
if (transcriptSessionId === activeTerminalSessionId.value) return
transitioning.value = true
transitionError.value = null
// Park current
parkCurrentTerminal()
// Small delay so the fade-out is visible before swapping content
await new Promise(r => setTimeout(r, 150))
// Find the entry — might be from another agent
const entry = serverRegistry.value.find(
e => e.transcriptSessionId === transcriptSessionId
@@ -247,12 +254,28 @@ export function useTranscriptDebug() {
await fetchSessions()
}
// Load the target session's transcript
// Load the target session's transcript (skip for __new__ — no transcript yet)
selectedSessionId.value = transcriptSessionId
await fetchSessionContent(transcriptSessionId)
if (transcriptSessionId === '__new__') {
// Brand-new session: no transcript to load, just clear conversation
rawContent.value = ''
conversation.value = null
error.value = null
} else {
const ok = await fetchSessionContent(transcriptSessionId)
if (!ok) {
transitionError.value = error.value || `Failed to load session "${transcriptSessionId}"`
transitioning.value = false
// Still connect to the terminal so the user can interact
connectToTerminal(transcriptSessionId)
return
}
}
// Connect to the terminal
connectToTerminal(transcriptSessionId)
transitioning.value = false
}
async function disposeAllLocalTerminals() {
@@ -583,6 +606,7 @@ export function useTranscriptDebug() {
selectedAgent.value = agent
error.value = null
transitionError.value = null
loading.value = true
transitioning.value = true
@@ -618,6 +642,7 @@ export function useTranscriptDebug() {
parkCurrentTerminal()
error.value = null
transitionError.value = null
loading.value = true
transitioning.value = true
@@ -927,6 +952,7 @@ export function useTranscriptDebug() {
conversation,
loading,
transitioning,
transitionError,
error,
lineCount,
isRealtime,