@burdenoff/microfe-vibecontrols 2026.602.10 → 2026.608.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/OpenInBrowserEditorButton.js +22 -17
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/sessions/TerminalPanel.js +536 -413
- package/dist/components/sessions/TerminalPanel.js.map +1 -1
- package/dist/components/sessions/sessionShortcuts.js +30 -3
- package/dist/components/sessions/sessionShortcuts.js.map +1 -1
- package/dist/components/shared/SessionNotificationBanner.js +28 -21
- package/dist/components/shared/SessionNotificationBanner.js.map +1 -1
- package/dist/pages/SessionsPage.js +1126 -908
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/store/vibeControlsStore.js +18 -0
- package/dist/store/vibeControlsStore.js.map +1 -1
- package/dist/translations/en.js +5 -0
- package/dist/translations/en.js.map +1 -1
- package/dist/utils/randomSessionName.js +55 -0
- package/dist/utils/randomSessionName.js.map +1 -0
- package/package.json +2 -2
|
@@ -12,9 +12,16 @@ function t(e, t) {
|
|
|
12
12
|
id: "new-session",
|
|
13
13
|
keysDisplay: "⌘/Ctrl + T",
|
|
14
14
|
category: "Actions",
|
|
15
|
-
description: t("sessions.sc.newSession", "New
|
|
15
|
+
description: t("sessions.sc.newSession", "New terminal (quick)"),
|
|
16
16
|
run: e.newSession
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
id: "new-session-options",
|
|
20
|
+
keysDisplay: "⌘/Ctrl + Shift + T",
|
|
21
|
+
category: "Actions",
|
|
22
|
+
description: t("sessions.sc.newSessionOptions", "New session with options…"),
|
|
23
|
+
run: e.newSessionWithOptions
|
|
24
|
+
},
|
|
18
25
|
{
|
|
19
26
|
id: "open-palette",
|
|
20
27
|
keysDisplay: "⌘/Ctrl + K",
|
|
@@ -29,6 +36,20 @@ function t(e, t) {
|
|
|
29
36
|
description: t("sessions.sc.reconnect", "Reconnect active terminal"),
|
|
30
37
|
run: e.reconnectActive
|
|
31
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: "rename-tab",
|
|
41
|
+
keysDisplay: "F2",
|
|
42
|
+
category: "Tabs",
|
|
43
|
+
description: t("sessions.sc.renameTab", "Rename active tab"),
|
|
44
|
+
run: e.renameActiveTab
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "open-tab-new",
|
|
48
|
+
keysDisplay: "⌘/Ctrl + O",
|
|
49
|
+
category: "Tabs",
|
|
50
|
+
description: t("sessions.sc.openTabNew", "Open active tab in new browser tab"),
|
|
51
|
+
run: e.openActiveInNewTab
|
|
52
|
+
},
|
|
32
53
|
{
|
|
33
54
|
id: "close-tab",
|
|
34
55
|
keysDisplay: "⌘/Ctrl + W",
|
|
@@ -38,13 +59,13 @@ function t(e, t) {
|
|
|
38
59
|
},
|
|
39
60
|
{
|
|
40
61
|
id: "next-tab",
|
|
41
|
-
keysDisplay: "Ctrl + Tab",
|
|
62
|
+
keysDisplay: "Ctrl + Tab · ⌘/Ctrl + Shift + ]",
|
|
42
63
|
category: "Tabs",
|
|
43
64
|
description: t("sessions.sc.nextTab", "Next tab")
|
|
44
65
|
},
|
|
45
66
|
{
|
|
46
67
|
id: "prev-tab",
|
|
47
|
-
keysDisplay: "Ctrl + Shift + Tab",
|
|
68
|
+
keysDisplay: "Ctrl + Shift + Tab · ⌘/Ctrl + Shift + [",
|
|
48
69
|
category: "Tabs",
|
|
49
70
|
description: t("sessions.sc.prevTab", "Previous tab")
|
|
50
71
|
},
|
|
@@ -131,6 +152,12 @@ function t(e, t) {
|
|
|
131
152
|
description: t("sessions.sc.openTarget", "Open active session's target"),
|
|
132
153
|
run: e.openActiveTarget
|
|
133
154
|
},
|
|
155
|
+
{
|
|
156
|
+
id: "terminal-select-copy",
|
|
157
|
+
keysDisplay: "⇧ Shift + drag",
|
|
158
|
+
category: "Help",
|
|
159
|
+
description: t("sessions.sc.selectCopy", "Select & copy text in the terminal (iTerm2-style, even in tmux)")
|
|
160
|
+
},
|
|
134
161
|
{
|
|
135
162
|
id: "open-cheatsheet",
|
|
136
163
|
keysDisplay: "?",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionShortcuts.js","names":[],"sources":["../../../src/components/sessions/sessionShortcuts.ts"],"sourcesContent":["/**\n * Single source of truth for the Sessions page keyboard shortcuts.\n *\n * The same registry drives (a) the keydown handler, (b) the cheatsheet\n * overlay, and (c) the command palette — so a shortcut and its help text\n * can never drift apart. Each entry carries a display string (platform\n * neutral) plus the action to run.\n */\nexport type ShortcutCategory = 'Tabs' | 'View' | 'Navigation' | 'Actions' | 'Help';\n\nexport const SHORTCUT_CATEGORY_ORDER: ShortcutCategory[] = [\n 'Actions',\n 'Tabs',\n 'View',\n 'Navigation',\n 'Help',\n];\n\nexport interface SessionShortcut {\n id: string;\n /** Platform-neutral key display, e.g. \"⌘/Ctrl + T\". */\n keysDisplay: string;\n description: string;\n category: ShortcutCategory;\n /** Action to run. Display-only entries (e.g. tab digits) omit it. */\n run?: () => void;\n}\n\nexport interface SessionShortcutHandlers {\n newSession: () => void;\n closeActiveTab: () => void;\n openPalette: () => void;\n openCheatsheet: () => void;\n focusSearch: () => void;\n cycleView: () => void;\n groupByNone: () => void;\n groupByAgent: () => void;\n groupByTarget: () => void;\n groupByVibe: () => void;\n groupByStatus: () => void;\n reconnectActive: () => void;\n toggleFullscreen: () => void;\n toggleInfoPanel: () => void;\n openActiveAgent: () => void;\n openActiveTarget: () => void;\n}\n\ntype Tr = (key: string, fallback: string) => string;\n\n/**\n * Build the shortcut registry. Key-event → id matching lives in the page's\n * keydown handler; this provides the canonical id/description/action set.\n */\nexport function buildSessionShortcuts(h: SessionShortcutHandlers, tr: Tr): SessionShortcut[] {\n return [\n // Actions\n {\n id: 'new-session',\n keysDisplay: '⌘/Ctrl + T',\n category: 'Actions',\n description: tr('sessions.sc.newSession', 'New
|
|
1
|
+
{"version":3,"file":"sessionShortcuts.js","names":[],"sources":["../../../src/components/sessions/sessionShortcuts.ts"],"sourcesContent":["/**\n * Single source of truth for the Sessions page keyboard shortcuts.\n *\n * The same registry drives (a) the keydown handler, (b) the cheatsheet\n * overlay, and (c) the command palette — so a shortcut and its help text\n * can never drift apart. Each entry carries a display string (platform\n * neutral) plus the action to run.\n */\nexport type ShortcutCategory = 'Tabs' | 'View' | 'Navigation' | 'Actions' | 'Help';\n\nexport const SHORTCUT_CATEGORY_ORDER: ShortcutCategory[] = [\n 'Actions',\n 'Tabs',\n 'View',\n 'Navigation',\n 'Help',\n];\n\nexport interface SessionShortcut {\n id: string;\n /** Platform-neutral key display, e.g. \"⌘/Ctrl + T\". */\n keysDisplay: string;\n description: string;\n category: ShortcutCategory;\n /** Action to run. Display-only entries (e.g. tab digits) omit it. */\n run?: () => void;\n}\n\nexport interface SessionShortcutHandlers {\n newSession: () => void;\n newSessionWithOptions: () => void;\n renameActiveTab: () => void;\n openActiveInNewTab: () => void;\n closeActiveTab: () => void;\n openPalette: () => void;\n openCheatsheet: () => void;\n focusSearch: () => void;\n cycleView: () => void;\n groupByNone: () => void;\n groupByAgent: () => void;\n groupByTarget: () => void;\n groupByVibe: () => void;\n groupByStatus: () => void;\n reconnectActive: () => void;\n toggleFullscreen: () => void;\n toggleInfoPanel: () => void;\n openActiveAgent: () => void;\n openActiveTarget: () => void;\n}\n\ntype Tr = (key: string, fallback: string) => string;\n\n/**\n * Build the shortcut registry. Key-event → id matching lives in the page's\n * keydown handler; this provides the canonical id/description/action set.\n */\nexport function buildSessionShortcuts(h: SessionShortcutHandlers, tr: Tr): SessionShortcut[] {\n return [\n // Actions\n {\n id: 'new-session',\n keysDisplay: '⌘/Ctrl + T',\n category: 'Actions',\n description: tr('sessions.sc.newSession', 'New terminal (quick)'),\n run: h.newSession,\n },\n {\n id: 'new-session-options',\n keysDisplay: '⌘/Ctrl + Shift + T',\n category: 'Actions',\n description: tr('sessions.sc.newSessionOptions', 'New session with options…'),\n run: h.newSessionWithOptions,\n },\n {\n id: 'open-palette',\n keysDisplay: '⌘/Ctrl + K',\n category: 'Actions',\n description: tr('sessions.sc.palette', 'Command palette'),\n run: h.openPalette,\n },\n {\n id: 'reconnect-active',\n keysDisplay: '⌘/Ctrl + R',\n category: 'Actions',\n description: tr('sessions.sc.reconnect', 'Reconnect active terminal'),\n run: h.reconnectActive,\n },\n // Tabs\n {\n id: 'rename-tab',\n keysDisplay: 'F2',\n category: 'Tabs',\n description: tr('sessions.sc.renameTab', 'Rename active tab'),\n run: h.renameActiveTab,\n },\n {\n id: 'open-tab-new',\n keysDisplay: '⌘/Ctrl + O',\n category: 'Tabs',\n description: tr('sessions.sc.openTabNew', 'Open active tab in new browser tab'),\n run: h.openActiveInNewTab,\n },\n {\n id: 'close-tab',\n keysDisplay: '⌘/Ctrl + W',\n category: 'Tabs',\n description: tr('sessions.sc.closeTab', 'Close active tab'),\n run: h.closeActiveTab,\n },\n {\n id: 'next-tab',\n keysDisplay: 'Ctrl + Tab · ⌘/Ctrl + Shift + ]',\n category: 'Tabs',\n description: tr('sessions.sc.nextTab', 'Next tab'),\n },\n {\n id: 'prev-tab',\n keysDisplay: 'Ctrl + Shift + Tab · ⌘/Ctrl + Shift + [',\n category: 'Tabs',\n description: tr('sessions.sc.prevTab', 'Previous tab'),\n },\n {\n id: 'jump-tab',\n keysDisplay: '⌘/Ctrl + 1…9',\n category: 'Tabs',\n description: tr('sessions.sc.jumpTab', 'Jump to tab N'),\n },\n // View\n {\n id: 'focus-search',\n keysDisplay: '/',\n category: 'View',\n description: tr('sessions.sc.focusSearch', 'Focus search'),\n run: h.focusSearch,\n },\n {\n id: 'cycle-view',\n keysDisplay: '⌘/Ctrl + \\\\',\n category: 'View',\n description: tr('sessions.sc.cycleView', 'Cycle terminal / grid / list'),\n run: h.cycleView,\n },\n {\n id: 'group-agent',\n keysDisplay: 'g a',\n category: 'View',\n description: tr('sessions.sc.groupAgent', 'Group by agent'),\n run: h.groupByAgent,\n },\n {\n id: 'group-target',\n keysDisplay: 'g t',\n category: 'View',\n description: tr('sessions.sc.groupTarget', 'Group by target'),\n run: h.groupByTarget,\n },\n {\n id: 'group-vibe',\n keysDisplay: 'g v',\n category: 'View',\n description: tr('sessions.sc.groupVibe', 'Group by vibe'),\n run: h.groupByVibe,\n },\n {\n id: 'group-status',\n keysDisplay: 'g s',\n category: 'View',\n description: tr('sessions.sc.groupStatus', 'Group by status'),\n run: h.groupByStatus,\n },\n {\n id: 'group-none',\n keysDisplay: 'g 0',\n category: 'View',\n description: tr('sessions.sc.groupNone', 'Clear grouping'),\n run: h.groupByNone,\n },\n {\n id: 'toggle-fullscreen',\n keysDisplay: 'f',\n category: 'View',\n description: tr('sessions.sc.fullscreen', 'Toggle terminal fullscreen'),\n run: h.toggleFullscreen,\n },\n {\n id: 'toggle-info',\n keysDisplay: 'i',\n category: 'View',\n description: tr('sessions.sc.infoPanel', 'Toggle session info panel'),\n run: h.toggleInfoPanel,\n },\n // Navigation\n {\n id: 'open-agent',\n keysDisplay: 'g shift+a',\n category: 'Navigation',\n description: tr('sessions.sc.openAgent', \"Open active session's agent\"),\n run: h.openActiveAgent,\n },\n {\n id: 'open-target',\n keysDisplay: 'g shift+t',\n category: 'Navigation',\n description: tr('sessions.sc.openTarget', \"Open active session's target\"),\n run: h.openActiveTarget,\n },\n // Help\n {\n id: 'terminal-select-copy',\n keysDisplay: '⇧ Shift + drag',\n category: 'Help',\n description: tr(\n 'sessions.sc.selectCopy',\n 'Select & copy text in the terminal (iTerm2-style, even in tmux)'\n ),\n },\n {\n id: 'open-cheatsheet',\n keysDisplay: '?',\n category: 'Help',\n description: tr('sessions.sc.cheatsheet', 'Keyboard shortcuts'),\n run: h.openCheatsheet,\n },\n ];\n}\n\n/** Group a registry into ordered [category, shortcuts] pairs for display. */\nexport function groupShortcutsByCategory(\n shortcuts: SessionShortcut[]\n): Array<[ShortcutCategory, SessionShortcut[]]> {\n return SHORTCUT_CATEGORY_ORDER.map(\n (category) =>\n [category, shortcuts.filter((s) => s.category === category)] as [\n ShortcutCategory,\n SessionShortcut[],\n ]\n ).filter(([, items]) => items.length > 0);\n}\n"],"mappings":"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"}
|
|
@@ -181,53 +181,60 @@ function T({ notification: e, onDismiss: t, autoDismiss: n = !0, autoDismissDela
|
|
|
181
181
|
]
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
function E() {
|
|
185
|
+
let { connectionStatus: e, reconnect: n, agentId: r } = t();
|
|
186
|
+
return r ? /* @__PURE__ */ _(y, {
|
|
187
|
+
status: e,
|
|
188
|
+
onReconnect: n
|
|
189
|
+
}) : null;
|
|
190
|
+
}
|
|
191
|
+
var D = 50;
|
|
192
|
+
function O({ maxVisible: o = 3, autoDismiss: s = !1, autoDismissDelay: c = 8e3, showConnectionStatus: l = !0, compact: u = !1 }) {
|
|
193
|
+
let d = e(), { notifications: f, connectionStatus: p, reconnect: m, agentId: h } = t(), [g, b] = a([]), x = i(/* @__PURE__ */ new Set());
|
|
187
194
|
r(() => {
|
|
188
195
|
let e = [];
|
|
189
|
-
for (let t of
|
|
190
|
-
e.length > 0 &&
|
|
191
|
-
}, [
|
|
192
|
-
let
|
|
193
|
-
|
|
194
|
-
}, []),
|
|
195
|
-
return !
|
|
196
|
-
className: "mb-4 space-y-3",
|
|
196
|
+
for (let t of f) x.current.has(t.id) || (x.current.add(t.id), e.push(t));
|
|
197
|
+
e.length > 0 && b((t) => [...e, ...t].slice(0, D));
|
|
198
|
+
}, [f]);
|
|
199
|
+
let S = n((e) => {
|
|
200
|
+
b((t) => t.filter((t) => t.id !== e));
|
|
201
|
+
}, []), C = g.slice(0, o), w = Math.max(0, g.length - o);
|
|
202
|
+
return !h && g.length === 0 || u && g.length === 0 ? null : /* @__PURE__ */ v("div", {
|
|
203
|
+
className: u ? "space-y-2" : "mb-4 space-y-3",
|
|
197
204
|
"data-testid": "session-notifications-container",
|
|
198
205
|
children: [
|
|
199
|
-
l &&
|
|
206
|
+
!u && l && h && /* @__PURE__ */ v("div", {
|
|
200
207
|
className: "flex items-center justify-between",
|
|
201
208
|
children: [/* @__PURE__ */ _("div", {
|
|
202
209
|
className: "text-xs text-text-muted",
|
|
203
|
-
children:
|
|
210
|
+
children: d("vibecontrols.shared.sessionNotificationBanner.agentNotifications", "Agent Notifications")
|
|
204
211
|
}), /* @__PURE__ */ _(y, {
|
|
205
|
-
status:
|
|
206
|
-
onReconnect:
|
|
212
|
+
status: p,
|
|
213
|
+
onReconnect: m
|
|
207
214
|
})]
|
|
208
215
|
}),
|
|
209
216
|
/* @__PURE__ */ _("div", {
|
|
210
217
|
className: "space-y-2",
|
|
211
|
-
children:
|
|
218
|
+
children: C.map((e) => /* @__PURE__ */ _(T, {
|
|
212
219
|
notification: e,
|
|
213
|
-
onDismiss:
|
|
220
|
+
onDismiss: S,
|
|
214
221
|
autoDismiss: s,
|
|
215
222
|
autoDismissDelay: c
|
|
216
223
|
}, e.id))
|
|
217
224
|
}),
|
|
218
|
-
|
|
225
|
+
w > 0 && /* @__PURE__ */ v("div", {
|
|
219
226
|
className: "text-xs text-text-muted text-center py-1",
|
|
220
227
|
children: [
|
|
221
228
|
"+",
|
|
222
|
-
|
|
229
|
+
w,
|
|
223
230
|
" ",
|
|
224
|
-
|
|
231
|
+
d("vibecontrols.shared.sessionNotificationBanner.moreNotifications", "more notification(s)")
|
|
225
232
|
]
|
|
226
233
|
})
|
|
227
234
|
]
|
|
228
235
|
});
|
|
229
236
|
}
|
|
230
237
|
//#endregion
|
|
231
|
-
export { y as ConnectionStatusIndicator,
|
|
238
|
+
export { y as ConnectionStatusIndicator, E as SessionConnectionStatus, O as default };
|
|
232
239
|
|
|
233
240
|
//# sourceMappingURL=SessionNotificationBanner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionNotificationBanner.js","names":[],"sources":["../../../src/components/shared/SessionNotificationBanner.tsx"],"sourcesContent":["/**\n * SessionNotificationBanner\n *\n * Displays AI tool notifications contextually within the Sessions page,\n * positioned above the session list for immediate visibility.\n *\n * Features:\n * - Source-specific styling (OpenCode, Claude Code, Gemini, Cursor)\n * - Event-type icons (success, error, info)\n * - Separate notification state from global toasts (no auto-dismiss interference)\n * - Manual dismiss button\n * - Smooth enter/exit animations\n *\n * Architecture:\n * - SessionNotifications maintains its OWN notification state separate from the global toast system\n * - It subscribes to the same WebSocket events but stores notifications independently\n * - This allows global toasts to auto-dismiss without affecting session banners\n */\nimport { useEffect, useState, useCallback, useRef } from 'react';\nimport {\n Bell,\n CheckCircle,\n AlertCircle,\n Info,\n X,\n Sparkles,\n Terminal,\n Zap,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport type { AiToolNotification, ConnectionStatus } from '../../hooks/useAgentEvents';\nimport { useAgentEventsContext } from '../../providers/AgentEventsProvider';\nimport { useTr } from '../../shared/hooks/useTr';\n\n/**\n * Props for the connection status indicator\n */\ninterface ConnectionStatusIndicatorProps {\n status: ConnectionStatus;\n onReconnect: () => void;\n}\n\n/**\n * Connection status indicator showing WebSocket state\n */\nexport function ConnectionStatusIndicator({ status, onReconnect }: ConnectionStatusIndicatorProps) {\n const tr = useTr();\n\n if (status === 'disconnected') {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-primary text-sm\">\n {status === 'connected' && (\n <>\n <Wifi className=\"size-4 text-status-success-text\" />\n <span className=\"text-status-success-text font-medium\">\n {tr('vibecontrols.shared.sessionNotificationBanner.live', 'Live')}\n </span>\n </>\n )}\n {status === 'connecting' && (\n <>\n <Wifi className=\"size-4 text-status-warning-text animate-pulse\" />\n <span className=\"text-status-warning-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.connecting', 'Connecting...')}\n </span>\n </>\n )}\n {status === 'error' && (\n <>\n <WifiOff className=\"size-4 text-status-error-text\" />\n <span className=\"text-status-error-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.disconnected', 'Disconnected')}\n </span>\n <button\n type=\"button\"\n onClick={onReconnect}\n className=\"ml-2 px-2 py-0.5 text-xs rounded bg-accent-primary/10 text-accent-primary hover:bg-accent-primary/20 transition-colors\"\n >\n {tr('vibecontrols.shared.sessionNotificationBanner.reconnect', 'Reconnect')}\n </button>\n </>\n )}\n </div>\n );\n}\n\n/**\n * Props for a single notification banner\n */\ninterface NotificationBannerProps {\n notification: AiToolNotification;\n onDismiss: (id: string) => void;\n autoDismiss?: boolean;\n autoDismissDelay?: number;\n}\n\n/**\n * Get the icon for a source tool\n */\nfunction getSourceIcon(source: string) {\n const iconClass = 'size-5';\n\n switch (source.toLowerCase()) {\n case 'opencode':\n return <Terminal className={iconClass} />;\n case 'claude-code':\n case 'claudecode':\n return <Sparkles className={iconClass} />;\n case 'gemini-cli':\n case 'gemini':\n return <Zap className={iconClass} />;\n case 'cursor':\n return <Terminal className={iconClass} />;\n default:\n return <Bell className={iconClass} />;\n }\n}\n\n/**\n * Get the icon for an event type\n */\nfunction getEventIcon(eventType: string) {\n const iconClass = 'size-4';\n\n if (\n eventType.includes('completed') ||\n eventType.includes('success') ||\n eventType.includes('idle')\n ) {\n return <CheckCircle className={`${iconClass} text-status-success-text`} />;\n }\n if (eventType.includes('failed') || eventType.includes('error')) {\n return <AlertCircle className={`${iconClass} text-status-error-text`} />;\n }\n if (eventType.includes('question') || eventType.includes('permission')) {\n return <AlertCircle className={`${iconClass} text-status-warning-text`} />;\n }\n return <Info className={`${iconClass} text-status-info-text`} />;\n}\n\n/**\n * Get display name for a source tool\n */\nfunction getSourceDisplayName(source: string): string {\n const names: Record<string, string> = {\n opencode: 'OpenCode',\n 'opencode-vibe-webhook': 'OpenCode',\n 'claude-code': 'Claude Code',\n claudecode: 'Claude Code',\n 'gemini-cli': 'Gemini CLI',\n gemini: 'Gemini CLI',\n cursor: 'Cursor',\n };\n return names[source.toLowerCase()] || source;\n}\n\n/**\n * Get background color based on source\n */\nfunction getSourceStyles(source: string): { bg: string; border: string; text: string } {\n switch (source.toLowerCase()) {\n case 'opencode':\n case 'opencode-vibe-webhook':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'claude-code':\n case 'claudecode':\n return {\n bg: 'bg-status-warning-bg/10',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n };\n case 'gemini-cli':\n case 'gemini':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'cursor':\n return {\n bg: 'bg-status-success-bg/10',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n };\n default:\n return {\n bg: 'bg-bg-secondary',\n border: 'border-border-primary',\n text: 'text-text-secondary',\n };\n }\n}\n\n/**\n * Format timestamp for display\n */\nfunction formatTimestamp(timestamp: string): string {\n try {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n } catch {\n return '';\n }\n}\n\n/**\n * Single notification banner\n */\nfunction NotificationBanner({\n notification,\n onDismiss,\n autoDismiss = true,\n autoDismissDelay = 8000,\n}: NotificationBannerProps) {\n const [isExiting, setIsExiting] = useState(false);\n const [progress, setProgress] = useState(100);\n const styles = getSourceStyles(notification.source);\n\n useEffect(() => {\n if (!autoDismiss) return;\n\n // Progress countdown\n const startTime = Date.now();\n const interval = setInterval(() => {\n const elapsed = Date.now() - startTime;\n const remaining = Math.max(0, 100 - (elapsed / autoDismissDelay) * 100);\n setProgress(remaining);\n }, 50);\n\n // Auto-dismiss timer\n const timer = setTimeout(() => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n }, autoDismissDelay);\n\n return () => {\n clearInterval(interval);\n clearTimeout(timer);\n };\n }, [autoDismiss, autoDismissDelay, notification.id, onDismiss]);\n\n const handleDismiss = () => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n };\n\n return (\n <div\n className={`\n relative flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm\n ${styles.bg} ${styles.border}\n transform transition-all duration-300 ease-out\n ${isExiting ? 'opacity-0 -translate-y-2 scale-95' : 'opacity-100 translate-y-0 scale-100'}\n animate-slide-in-down\n `}\n role=\"alert\"\n data-testid=\"session-notification-banner\"\n >\n {/* Source icon */}\n <div className={`flex-shrink-0 p-2 rounded-lg bg-bg-tertiary/50 ${styles.text}`}>\n {getSourceIcon(notification.source)}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Header */}\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs font-semibold uppercase tracking-wide ${styles.text}`}>\n {getSourceDisplayName(notification.source)}\n </span>\n {getEventIcon(notification.eventType)}\n <span className=\"text-xs text-text-muted\">{formatTimestamp(notification.timestamp)}</span>\n </div>\n\n {/* Title */}\n <p className=\"text-sm font-medium text-text-primary\">{notification.title}</p>\n\n {/* Message */}\n <p className=\"text-xs text-text-secondary mt-0.5\">{notification.message}</p>\n </div>\n\n {/* Dismiss button */}\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"flex-shrink-0 p-1 rounded-md hover:bg-bg-tertiary/50 text-text-muted hover:text-text-secondary transition-colors\"\n aria-label=\"Dismiss notification\"\n >\n <X className=\"size-4\" />\n </button>\n\n {/* Progress bar for auto-dismiss */}\n {autoDismiss && (\n <div className=\"absolute bottom-0 left-0 right-0 h-0.5 bg-bg-tertiary/30 overflow-hidden rounded-b-lg\">\n <div\n className={`h-full ${styles.text.replace('text-', 'bg-')}/50 transition-all duration-50 ease-linear`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Props for the session notifications container\n */\nexport interface SessionNotificationsProps {\n /**\n * Maximum number of notifications to display\n */\n maxVisible?: number;\n\n /**\n * Whether notifications should auto-dismiss (default: false - requires manual close)\n */\n autoDismiss?: boolean;\n\n /**\n * Auto-dismiss delay in milliseconds (only used if autoDismiss is true)\n */\n autoDismissDelay?: number;\n\n /**\n * Whether to show the connection status indicator\n */\n showConnectionStatus?: boolean;\n}\n\n/**\n * Maximum notifications to keep in session banner state\n */\nconst MAX_SESSION_NOTIFICATIONS = 50;\n\n/**\n * Container component for session-contextual notifications\n * Displays notifications above the session list with connection status\n *\n * IMPORTANT: This component maintains its OWN notification state separate from the global\n * toast system. It subscribes to the same WebSocket events via the context but stores\n * notifications independently. This allows:\n * - Global toasts to auto-dismiss without affecting session banners\n * - Session banners to persist until manually dismissed by the user\n */\nexport function SessionNotifications({\n maxVisible = 3,\n autoDismiss = false,\n autoDismissDelay = 8000,\n showConnectionStatus = true,\n}: SessionNotificationsProps) {\n const tr = useTr();\n const {\n notifications: globalNotifications,\n connectionStatus,\n reconnect,\n agentId,\n } = useAgentEventsContext();\n\n // Maintain separate notification state for session banners\n const [sessionNotifications, setSessionNotifications] = useState<AiToolNotification[]>([]);\n const seenIdsRef = useRef<Set<string>>(new Set());\n\n // Sync new notifications from global state to session state\n // We only ADD new notifications, never remove them (except via manual dismiss)\n useEffect(() => {\n const newNotifications: AiToolNotification[] = [];\n\n for (const notification of globalNotifications) {\n if (!seenIdsRef.current.has(notification.id)) {\n seenIdsRef.current.add(notification.id);\n newNotifications.push(notification);\n }\n }\n\n if (newNotifications.length > 0) {\n setSessionNotifications((prev) => {\n const updated = [...newNotifications, ...prev];\n return updated.slice(0, MAX_SESSION_NOTIFICATIONS);\n });\n }\n }, [globalNotifications]);\n\n // Handle manual dismiss - only affects session banner state\n const handleDismiss = useCallback((id: string) => {\n setSessionNotifications((prev) => prev.filter((n) => n.id !== id));\n }, []);\n\n const visibleNotifications = sessionNotifications.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sessionNotifications.length - maxVisible);\n\n // Don't render anything if no agent is configured\n if (!agentId && sessionNotifications.length === 0) {\n return null;\n }\n\n return (\n <div className=\"mb-4 space-y-3\" data-testid=\"session-notifications-container\">\n {/* Connection status indicator */}\n {showConnectionStatus && agentId && (\n <div className=\"flex items-center justify-between\">\n <div className=\"text-xs text-text-muted\">\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.agentNotifications',\n 'Agent Notifications'\n )}\n </div>\n <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />\n </div>\n )}\n\n {/* Notification banners */}\n <div className=\"space-y-2\">\n {visibleNotifications.map((notification) => (\n <NotificationBanner\n key={notification.id}\n notification={notification}\n onDismiss={handleDismiss}\n autoDismiss={autoDismiss}\n autoDismissDelay={autoDismissDelay}\n />\n ))}\n </div>\n\n {/* Hidden count indicator */}\n {hiddenCount > 0 && (\n <div className=\"text-xs text-text-muted text-center py-1\">\n +{hiddenCount}{' '}\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.moreNotifications',\n 'more notification(s)'\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default SessionNotifications;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionNotificationBanner.js","names":[],"sources":["../../../src/components/shared/SessionNotificationBanner.tsx"],"sourcesContent":["/**\n * SessionNotificationBanner\n *\n * Displays AI tool notifications contextually within the Sessions page,\n * positioned above the session list for immediate visibility.\n *\n * Features:\n * - Source-specific styling (OpenCode, Claude Code, Gemini, Cursor)\n * - Event-type icons (success, error, info)\n * - Separate notification state from global toasts (no auto-dismiss interference)\n * - Manual dismiss button\n * - Smooth enter/exit animations\n *\n * Architecture:\n * - SessionNotifications maintains its OWN notification state separate from the global toast system\n * - It subscribes to the same WebSocket events but stores notifications independently\n * - This allows global toasts to auto-dismiss without affecting session banners\n */\nimport { useEffect, useState, useCallback, useRef } from 'react';\nimport {\n Bell,\n CheckCircle,\n AlertCircle,\n Info,\n X,\n Sparkles,\n Terminal,\n Zap,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport type { AiToolNotification, ConnectionStatus } from '../../hooks/useAgentEvents';\nimport { useAgentEventsContext } from '../../providers/AgentEventsProvider';\nimport { useTr } from '../../shared/hooks/useTr';\n\n/**\n * Props for the connection status indicator\n */\ninterface ConnectionStatusIndicatorProps {\n status: ConnectionStatus;\n onReconnect: () => void;\n}\n\n/**\n * Connection status indicator showing WebSocket state\n */\nexport function ConnectionStatusIndicator({ status, onReconnect }: ConnectionStatusIndicatorProps) {\n const tr = useTr();\n\n if (status === 'disconnected') {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-primary text-sm\">\n {status === 'connected' && (\n <>\n <Wifi className=\"size-4 text-status-success-text\" />\n <span className=\"text-status-success-text font-medium\">\n {tr('vibecontrols.shared.sessionNotificationBanner.live', 'Live')}\n </span>\n </>\n )}\n {status === 'connecting' && (\n <>\n <Wifi className=\"size-4 text-status-warning-text animate-pulse\" />\n <span className=\"text-status-warning-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.connecting', 'Connecting...')}\n </span>\n </>\n )}\n {status === 'error' && (\n <>\n <WifiOff className=\"size-4 text-status-error-text\" />\n <span className=\"text-status-error-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.disconnected', 'Disconnected')}\n </span>\n <button\n type=\"button\"\n onClick={onReconnect}\n className=\"ml-2 px-2 py-0.5 text-xs rounded bg-accent-primary/10 text-accent-primary hover:bg-accent-primary/20 transition-colors\"\n >\n {tr('vibecontrols.shared.sessionNotificationBanner.reconnect', 'Reconnect')}\n </button>\n </>\n )}\n </div>\n );\n}\n\n/**\n * Props for a single notification banner\n */\ninterface NotificationBannerProps {\n notification: AiToolNotification;\n onDismiss: (id: string) => void;\n autoDismiss?: boolean;\n autoDismissDelay?: number;\n}\n\n/**\n * Get the icon for a source tool\n */\nfunction getSourceIcon(source: string) {\n const iconClass = 'size-5';\n\n switch (source.toLowerCase()) {\n case 'opencode':\n return <Terminal className={iconClass} />;\n case 'claude-code':\n case 'claudecode':\n return <Sparkles className={iconClass} />;\n case 'gemini-cli':\n case 'gemini':\n return <Zap className={iconClass} />;\n case 'cursor':\n return <Terminal className={iconClass} />;\n default:\n return <Bell className={iconClass} />;\n }\n}\n\n/**\n * Get the icon for an event type\n */\nfunction getEventIcon(eventType: string) {\n const iconClass = 'size-4';\n\n if (\n eventType.includes('completed') ||\n eventType.includes('success') ||\n eventType.includes('idle')\n ) {\n return <CheckCircle className={`${iconClass} text-status-success-text`} />;\n }\n if (eventType.includes('failed') || eventType.includes('error')) {\n return <AlertCircle className={`${iconClass} text-status-error-text`} />;\n }\n if (eventType.includes('question') || eventType.includes('permission')) {\n return <AlertCircle className={`${iconClass} text-status-warning-text`} />;\n }\n return <Info className={`${iconClass} text-status-info-text`} />;\n}\n\n/**\n * Get display name for a source tool\n */\nfunction getSourceDisplayName(source: string): string {\n const names: Record<string, string> = {\n opencode: 'OpenCode',\n 'opencode-vibe-webhook': 'OpenCode',\n 'claude-code': 'Claude Code',\n claudecode: 'Claude Code',\n 'gemini-cli': 'Gemini CLI',\n gemini: 'Gemini CLI',\n cursor: 'Cursor',\n };\n return names[source.toLowerCase()] || source;\n}\n\n/**\n * Get background color based on source\n */\nfunction getSourceStyles(source: string): { bg: string; border: string; text: string } {\n switch (source.toLowerCase()) {\n case 'opencode':\n case 'opencode-vibe-webhook':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'claude-code':\n case 'claudecode':\n return {\n bg: 'bg-status-warning-bg/10',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n };\n case 'gemini-cli':\n case 'gemini':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'cursor':\n return {\n bg: 'bg-status-success-bg/10',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n };\n default:\n return {\n bg: 'bg-bg-secondary',\n border: 'border-border-primary',\n text: 'text-text-secondary',\n };\n }\n}\n\n/**\n * Format timestamp for display\n */\nfunction formatTimestamp(timestamp: string): string {\n try {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n } catch {\n return '';\n }\n}\n\n/**\n * Single notification banner\n */\nfunction NotificationBanner({\n notification,\n onDismiss,\n autoDismiss = true,\n autoDismissDelay = 8000,\n}: NotificationBannerProps) {\n const [isExiting, setIsExiting] = useState(false);\n const [progress, setProgress] = useState(100);\n const styles = getSourceStyles(notification.source);\n\n useEffect(() => {\n if (!autoDismiss) return;\n\n // Progress countdown\n const startTime = Date.now();\n const interval = setInterval(() => {\n const elapsed = Date.now() - startTime;\n const remaining = Math.max(0, 100 - (elapsed / autoDismissDelay) * 100);\n setProgress(remaining);\n }, 50);\n\n // Auto-dismiss timer\n const timer = setTimeout(() => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n }, autoDismissDelay);\n\n return () => {\n clearInterval(interval);\n clearTimeout(timer);\n };\n }, [autoDismiss, autoDismissDelay, notification.id, onDismiss]);\n\n const handleDismiss = () => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n };\n\n return (\n <div\n className={`\n relative flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm\n ${styles.bg} ${styles.border}\n transform transition-all duration-300 ease-out\n ${isExiting ? 'opacity-0 -translate-y-2 scale-95' : 'opacity-100 translate-y-0 scale-100'}\n animate-slide-in-down\n `}\n role=\"alert\"\n data-testid=\"session-notification-banner\"\n >\n {/* Source icon */}\n <div className={`flex-shrink-0 p-2 rounded-lg bg-bg-tertiary/50 ${styles.text}`}>\n {getSourceIcon(notification.source)}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Header */}\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs font-semibold uppercase tracking-wide ${styles.text}`}>\n {getSourceDisplayName(notification.source)}\n </span>\n {getEventIcon(notification.eventType)}\n <span className=\"text-xs text-text-muted\">{formatTimestamp(notification.timestamp)}</span>\n </div>\n\n {/* Title */}\n <p className=\"text-sm font-medium text-text-primary\">{notification.title}</p>\n\n {/* Message */}\n <p className=\"text-xs text-text-secondary mt-0.5\">{notification.message}</p>\n </div>\n\n {/* Dismiss button */}\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"flex-shrink-0 p-1 rounded-md hover:bg-bg-tertiary/50 text-text-muted hover:text-text-secondary transition-colors\"\n aria-label=\"Dismiss notification\"\n >\n <X className=\"size-4\" />\n </button>\n\n {/* Progress bar for auto-dismiss */}\n {autoDismiss && (\n <div className=\"absolute bottom-0 left-0 right-0 h-0.5 bg-bg-tertiary/30 overflow-hidden rounded-b-lg\">\n <div\n className={`h-full ${styles.text.replace('text-', 'bg-')}/50 transition-all duration-50 ease-linear`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Props for the session notifications container\n */\nexport interface SessionNotificationsProps {\n /**\n * Maximum number of notifications to display\n */\n maxVisible?: number;\n\n /**\n * Whether notifications should auto-dismiss (default: false - requires manual close)\n */\n autoDismiss?: boolean;\n\n /**\n * Auto-dismiss delay in milliseconds (only used if autoDismiss is true)\n */\n autoDismissDelay?: number;\n\n /**\n * Whether to show the connection status indicator\n */\n showConnectionStatus?: boolean;\n\n /**\n * Compact mode: drop the standalone \"Agent Notifications\" header row and\n * render nothing at all when there are no notifications, so the banner\n * stops reserving vertical space. The connection status is expected to be\n * rendered in the page header instead (see `SessionConnectionStatus`).\n */\n compact?: boolean;\n}\n\n/**\n * Standalone agent connection-status chip for placement in a page header.\n * Reclaims the row the notification banner used to spend on a dedicated\n * \"Agent Notifications\" + status line. Renders nothing when no agent is\n * configured for the current context.\n */\nexport function SessionConnectionStatus() {\n const { connectionStatus, reconnect, agentId } = useAgentEventsContext();\n if (!agentId) return null;\n return <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />;\n}\n\n/**\n * Maximum notifications to keep in session banner state\n */\nconst MAX_SESSION_NOTIFICATIONS = 50;\n\n/**\n * Container component for session-contextual notifications\n * Displays notifications above the session list with connection status\n *\n * IMPORTANT: This component maintains its OWN notification state separate from the global\n * toast system. It subscribes to the same WebSocket events via the context but stores\n * notifications independently. This allows:\n * - Global toasts to auto-dismiss without affecting session banners\n * - Session banners to persist until manually dismissed by the user\n */\nexport function SessionNotifications({\n maxVisible = 3,\n autoDismiss = false,\n autoDismissDelay = 8000,\n showConnectionStatus = true,\n compact = false,\n}: SessionNotificationsProps) {\n const tr = useTr();\n const {\n notifications: globalNotifications,\n connectionStatus,\n reconnect,\n agentId,\n } = useAgentEventsContext();\n\n // Maintain separate notification state for session banners\n const [sessionNotifications, setSessionNotifications] = useState<AiToolNotification[]>([]);\n const seenIdsRef = useRef<Set<string>>(new Set());\n\n // Sync new notifications from global state to session state\n // We only ADD new notifications, never remove them (except via manual dismiss)\n useEffect(() => {\n const newNotifications: AiToolNotification[] = [];\n\n for (const notification of globalNotifications) {\n if (!seenIdsRef.current.has(notification.id)) {\n seenIdsRef.current.add(notification.id);\n newNotifications.push(notification);\n }\n }\n\n if (newNotifications.length > 0) {\n setSessionNotifications((prev) => {\n const updated = [...newNotifications, ...prev];\n return updated.slice(0, MAX_SESSION_NOTIFICATIONS);\n });\n }\n }, [globalNotifications]);\n\n // Handle manual dismiss - only affects session banner state\n const handleDismiss = useCallback((id: string) => {\n setSessionNotifications((prev) => prev.filter((n) => n.id !== id));\n }, []);\n\n const visibleNotifications = sessionNotifications.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sessionNotifications.length - maxVisible);\n\n // Don't render anything if no agent is configured. In compact mode we also\n // render nothing while there are no notifications so the banner stops\n // reserving a wasted row (the connection status lives in the page header).\n if (!agentId && sessionNotifications.length === 0) {\n return null;\n }\n if (compact && sessionNotifications.length === 0) {\n return null;\n }\n\n return (\n <div\n className={compact ? 'space-y-2' : 'mb-4 space-y-3'}\n data-testid=\"session-notifications-container\"\n >\n {/* Connection status indicator — header row only in full mode. In\n compact mode the status is rendered in the page header instead. */}\n {!compact && showConnectionStatus && agentId && (\n <div className=\"flex items-center justify-between\">\n <div className=\"text-xs text-text-muted\">\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.agentNotifications',\n 'Agent Notifications'\n )}\n </div>\n <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />\n </div>\n )}\n\n {/* Notification banners */}\n <div className=\"space-y-2\">\n {visibleNotifications.map((notification) => (\n <NotificationBanner\n key={notification.id}\n notification={notification}\n onDismiss={handleDismiss}\n autoDismiss={autoDismiss}\n autoDismissDelay={autoDismissDelay}\n />\n ))}\n </div>\n\n {/* Hidden count indicator */}\n {hiddenCount > 0 && (\n <div className=\"text-xs text-text-muted text-center py-1\">\n +{hiddenCount}{' '}\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.moreNotifications',\n 'more notification(s)'\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default SessionNotifications;\n"],"mappings":"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"}
|