@@ -209,16 +154,21 @@ onBeforeUnmount(() => {
-
+
-
-
-
{{ activeEvent }}
-
{{ activeDetail }}
+
+
+
{{ tooltipText }}
@@ -288,49 +238,54 @@ onBeforeUnmount(() => {
transition: transform 0.2s ease;
}
-/* ── Current event ── */
+/* ── Icon + tooltip ── */
-.lc-content {
+.lc-icon-wrap {
+ position: relative;
display: flex;
align-items: center;
- gap: 5px;
- flex: 1;
- min-width: 0;
-}
-
-.lc-dot {
- width: 5px;
- height: 5px;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
flex-shrink: 0;
- border-radius: 0;
- transition: background 0.3s ease;
+ cursor: default;
}
-.lc-dot.processing {
- animation: pulse-lifecycle 1.5s ease-in-out infinite;
+.lc-icon {
+ transition: transform 0.2s ease, filter 0.2s ease;
}
-@keyframes pulse-lifecycle {
- 0%, 100% { opacity: 0.5; transform: scale(0.8); }
- 50% { opacity: 1; transform: scale(1.2); }
+.lc-icon-wrap.processing .lc-icon {
+ animation: pulse-icon 1.5s ease-in-out infinite;
}
-.lc-event {
+@keyframes pulse-icon {
+ 0%, 100% { opacity: 0.5; transform: scale(0.85); }
+ 50% { opacity: 1; transform: scale(1.1); }
+}
+
+.lc-tooltip {
+ position: absolute;
+ bottom: calc(100% + 4px);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 3px 6px;
+ background: rgba(15, 15, 25, 0.95);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
font-size: 9px;
font-weight: 600;
font-family: 'Courier New', monospace;
+ color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
- transition: color 0.3s ease;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ z-index: 20;
}
-.lc-detail {
- font-size: 9px;
- font-family: 'Courier New', monospace;
- color: rgba(255, 255, 255, 0.3);
- white-space: nowrap;
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
+.lc-icon-wrap:hover .lc-tooltip {
+ opacity: 1;
}
/* Transition classes */
diff --git a/frontend/src/composables/transcript-debug/index.ts b/frontend/src/composables/transcript-debug/index.ts
index 82fc9ce..eda613d 100644
--- a/frontend/src/composables/transcript-debug/index.ts
+++ b/frontend/src/composables/transcript-debug/index.ts
@@ -1,2 +1 @@
export { useTranscriptDebug } from './useTranscriptDebug'
-export { useHooksApproval } from './useHooksApproval'
diff --git a/frontend/src/composables/transcript-debug/useHooksApproval.ts b/frontend/src/composables/transcript-debug/useHooksApproval.ts
deleted file mode 100644
index b1e11b7..0000000
--- a/frontend/src/composables/transcript-debug/useHooksApproval.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { ref } from 'vue'
-import { apiFetch } from '@/lib/tauri'
-import type { HooksApprovalPermissionRequest, HooksApprovalPlanRequest } from '@/types/hooks-approval'
-
-export function useHooksApproval() {
- const pendingPermissions = ref([])
- const pendingPlans = ref([])
-
- // ── WS message handler (called from useTranscriptDebug) ──
-
- function handleApprovalMessage(msg: { type: string; [key: string]: unknown }) {
- if (msg.type === 'hooks-approval-permission') {
- pendingPermissions.value.push({
- requestId: msg.requestId as string,
- tool_name: msg.tool_name as string | undefined,
- tool_input: msg.tool_input,
- agent_name: msg.agent_name as string | undefined,
- session_id: msg.session_id as string | undefined,
- cwd: msg.cwd as string | undefined,
- timestamp: msg.timestamp as number
- })
- } else if (msg.type === 'hooks-approval-plan') {
- pendingPlans.value.push({
- requestId: msg.requestId as string,
- session_id: msg.session_id as string | undefined,
- permission_mode: msg.permission_mode as string | undefined,
- lastAssistantText: (msg.lastAssistantText as string) || '',
- timestamp: msg.timestamp as number
- })
- }
- }
-
- // ── Respond to permission ──
-
- async function respondPermission(requestId: string, decision: 'allow' | 'deny') {
- try {
- await apiFetch('/api/hooks-approval/respond', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ requestId, decision })
- })
- } catch (e) {
- console.error('[HooksApproval] Failed to respond permission:', e)
- }
- pendingPermissions.value = pendingPermissions.value.filter(p => p.requestId !== requestId)
- }
-
- // ── Respond to plan ──
-
- async function respondPlan(requestId: string, decision: 'approve' | 'reject' | 'edit', reason?: string) {
- try {
- await apiFetch('/api/hooks-approval/respond-plan', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ requestId, decision, reason })
- })
- } catch (e) {
- console.error('[HooksApproval] Failed to respond plan:', e)
- }
- pendingPlans.value = pendingPlans.value.filter(p => p.requestId !== requestId)
- }
-
- // ── Fetch pending on page load (state recovery) ──
-
- async function fetchPending() {
- try {
- const res = await apiFetch('/api/hooks-approval')
- if (!res.ok) return
- const data = await res.json()
-
- if (Array.isArray(data.permissions)) {
- for (const p of data.permissions) {
- if (!pendingPermissions.value.some(e => e.requestId === p.requestId)) {
- pendingPermissions.value.push({
- requestId: p.requestId,
- tool_name: p.tool_name,
- tool_input: p.tool_input,
- agent_name: p.agent_name,
- session_id: p.session_id,
- timestamp: p.createdAt
- })
- }
- }
- }
-
- if (Array.isArray(data.plans)) {
- for (const p of data.plans) {
- if (!pendingPlans.value.some(e => e.requestId === p.requestId)) {
- pendingPlans.value.push({
- requestId: p.requestId,
- session_id: p.session_id,
- permission_mode: p.permission_mode,
- lastAssistantText: p.lastAssistantText || '',
- timestamp: p.createdAt
- })
- }
- }
- }
- } catch (e) {
- console.error('[HooksApproval] Failed to fetch pending:', e)
- }
- }
-
- return {
- pendingPermissions,
- pendingPlans,
- handleApprovalMessage,
- respondPermission,
- respondPlan,
- fetchPending
- }
-}
diff --git a/server/services/session-state.ts b/server/services/session-state.ts
index 8513585..b41c576 100644
--- a/server/services/session-state.ts
+++ b/server/services/session-state.ts
@@ -322,8 +322,12 @@ class SessionStateManager {
// Build notification
const notification = buildNotification(payload)
- const updatedNotifications = [...state.notifications, notification].slice(-MAX_NOTIFICATIONS)
- patch.notifications = updatedNotifications
+ if (payload.hook_event_name === 'SessionStart') {
+ // Reset notifications on new session, only keep the SessionStart notification
+ patch.notifications = [notification]
+ } else {
+ patch.notifications = [...state.notifications, notification].slice(-MAX_NOTIFICATIONS)
+ }
// Build hook history entry
const historyEntry: HookHistoryEntry = {