@myrialabs/clopen 0.1.2 → 0.1.3

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.
Files changed (50) hide show
  1. package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
  2. package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
  3. package/backend/lib/project/status-manager.ts +221 -181
  4. package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
  5. package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
  6. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
  7. package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
  8. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
  9. package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
  10. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
  11. package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
  12. package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
  13. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
  14. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
  15. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
  16. package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
  17. package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
  18. package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
  19. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
  20. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
  21. package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
  22. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
  23. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
  24. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
  25. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
  26. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
  27. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
  28. package/frontend/lib/components/common/Button.svelte +1 -1
  29. package/frontend/lib/components/common/Card.svelte +3 -3
  30. package/frontend/lib/components/common/Input.svelte +3 -3
  31. package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
  32. package/frontend/lib/components/common/Select.svelte +6 -6
  33. package/frontend/lib/components/common/Textarea.svelte +3 -3
  34. package/frontend/lib/components/files/FileViewer.svelte +1 -1
  35. package/frontend/lib/components/git/ChangesSection.svelte +2 -4
  36. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
  37. package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
  38. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
  39. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
  40. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
  41. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
  42. package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
  43. package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
  44. package/frontend/lib/components/workspace/PanelHeader.svelte +115 -4
  45. package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
  46. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
  47. package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
  48. package/frontend/lib/services/project/status.service.ts +160 -159
  49. package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
  50. package/package.json +1 -1
@@ -7,9 +7,18 @@
7
7
  import { projectState } from '$frontend/lib/stores/core/projects.svelte';
8
8
  import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
9
9
  import { userStore } from '$frontend/lib/stores/features/user.svelte';
10
- import { workspaceState, type PanelId } from '$frontend/lib/stores/ui/workspace.svelte';
10
+ import {
11
+ workspaceState,
12
+ type PanelId,
13
+ swapPanel,
14
+ splitPanel,
15
+ closePanel,
16
+ canClosePanel,
17
+ PANEL_OPTIONS
18
+ } from '$frontend/lib/stores/ui/workspace.svelte';
11
19
  import type { IconName } from '$shared/types/ui/icons';
12
20
  import type { DeviceSize } from '$frontend/lib/constants/preview';
21
+
13
22
  import { DEVICE_VIEWPORTS } from '$frontend/lib/constants/preview';
14
23
 
15
24
  interface Props {
@@ -72,6 +81,33 @@
72
81
  }
73
82
  });
74
83
 
84
+ // Panel actions menu state
85
+ let showActionsMenu = $state(false);
86
+
87
+ function toggleActionsMenu(e: MouseEvent) {
88
+ e.stopPropagation();
89
+ showActionsMenu = !showActionsMenu;
90
+ }
91
+
92
+ function closeActionsMenu() {
93
+ showActionsMenu = false;
94
+ }
95
+
96
+ function handleSwap(newPanelId: PanelId) {
97
+ swapPanel(panelId, newPanelId);
98
+ closeActionsMenu();
99
+ }
100
+
101
+ function handleSplit(direction: 'vertical' | 'horizontal') {
102
+ splitPanel(panelId, direction);
103
+ closeActionsMenu();
104
+ }
105
+
106
+ function handleClose() {
107
+ closePanel(panelId);
108
+ closeActionsMenu();
109
+ }
110
+
75
111
  // Preview panel device dropdown state
76
112
  let showDeviceDropdown = $state(false);
77
113
 
@@ -116,9 +152,83 @@
116
152
  ? 'h-11 pb-2 px-4 bg-white/90 dark:bg-slate-900/98 border-b border-slate-200 dark:border-slate-800'
117
153
  : 'py-2.5 px-3.5 bg-slate-100 dark:bg-slate-800/80 border-b border-slate-200 dark:border-slate-800'}"
118
154
  >
119
- <div class="flex items-center gap-2.5 text-sm font-medium text-slate-900 dark:text-slate-100">
155
+ <div class="flex items-center text-sm font-medium text-slate-900 dark:text-slate-100">
156
+ <!-- Panel layout actions (⋮ menu) -->
157
+ {#if !isMobile}
158
+ <div class="relative -ml-0.5 mr-2 border-slate-200 dark:border-slate-700">
159
+ <button
160
+ type="button"
161
+ class="flex items-center justify-center w-6 h-6 bg-transparent border-none rounded-md text-slate-400 cursor-pointer transition-all duration-150 hover:bg-violet-500/10 hover:text-slate-700 dark:hover:text-slate-200"
162
+ onclick={toggleActionsMenu}
163
+ title="Panel actions"
164
+ >
165
+ <Icon name="lucide:ellipsis-vertical" class="w-3.5 h-3.5" />
166
+ </button>
167
+
168
+ {#if showActionsMenu}
169
+ <div class="fixed inset-0 z-40" onclick={closeActionsMenu}></div>
170
+ <div class="absolute top-full left-0 mt-1 z-50 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg overflow-hidden min-w-44 py-1">
171
+ <!-- Switch to section -->
172
+ <div class="px-3 py-1.5 text-3xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
173
+ Switch to
174
+ </div>
175
+ {#each PANEL_OPTIONS as option}
176
+ <button
177
+ type="button"
178
+ class="flex items-center gap-2.5 w-full px-3 py-1.5 text-left text-xs bg-transparent border-none cursor-pointer transition-all duration-150 hover:bg-violet-500/10
179
+ {option.id === panelId ? 'text-violet-600 dark:text-violet-400 font-medium' : 'text-slate-700 dark:text-slate-300'}"
180
+ onclick={() => handleSwap(option.id)}
181
+ disabled={option.id === panelId}
182
+ >
183
+ <Icon name={option.icon} class="w-3.5 h-3.5" />
184
+ <span class="flex-1">{option.title}</span>
185
+ {#if option.id === panelId}
186
+ <Icon name="lucide:check" class="w-3 h-3 text-violet-600 dark:text-violet-400" />
187
+ {/if}
188
+ </button>
189
+ {/each}
190
+
191
+ <!-- Divider -->
192
+ <div class="my-1 border-t border-slate-200 dark:border-slate-700"></div>
193
+
194
+ <!-- Split actions -->
195
+ <button
196
+ type="button"
197
+ class="flex items-center gap-2.5 w-full px-3 py-1.5 text-left text-xs bg-transparent border-none cursor-pointer transition-all duration-150 hover:bg-violet-500/10 text-slate-700 dark:text-slate-300"
198
+ onclick={() => handleSplit('vertical')}
199
+ >
200
+ <Icon name="lucide:columns-2" class="w-3.5 h-3.5" />
201
+ <span>Split Right</span>
202
+ </button>
203
+ <button
204
+ type="button"
205
+ class="flex items-center gap-2.5 w-full px-3 py-1.5 text-left text-xs bg-transparent border-none cursor-pointer transition-all duration-150 hover:bg-violet-500/10 text-slate-700 dark:text-slate-300"
206
+ onclick={() => handleSplit('horizontal')}
207
+ >
208
+ <Icon name="lucide:rows-2" class="w-3.5 h-3.5" />
209
+ <span>Split Down</span>
210
+ </button>
211
+
212
+ <!-- Divider -->
213
+ <div class="my-1 border-t border-slate-200 dark:border-slate-700"></div>
214
+
215
+ <!-- Close -->
216
+ <button
217
+ type="button"
218
+ class="flex items-center gap-2.5 w-full px-3 py-1.5 text-left text-xs bg-transparent border-none cursor-pointer transition-all duration-150 hover:bg-red-500/10 text-red-600 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed"
219
+ onclick={handleClose}
220
+ disabled={!canClosePanel()}
221
+ title={!canClosePanel() ? 'Cannot close last panel' : 'Close this panel'}
222
+ >
223
+ <Icon name="lucide:x" class="w-3.5 h-3.5" />
224
+ <span>Close Panel</span>
225
+ </button>
226
+ </div>
227
+ {/if}
228
+ </div>
229
+ {/if}
120
230
  <Icon name={iconName} class="w-4 h-4 text-violet-600" />
121
- <span>{panel?.title ?? 'Panel'}</span>
231
+ <span class="ml-2.5">{panel?.title ?? 'Panel'}</span>
122
232
  </div>
123
233
 
124
234
  <div class="flex items-center">
@@ -443,7 +553,7 @@
443
553
  <Icon name="lucide:globe" class="w-3.5 h-3.5 shrink-0" />
444
554
  <div class="flex-1 min-w-0">
445
555
  <div>{remote.name}</div>
446
- <div class="text-[10px] text-slate-400 truncate font-mono">{remote.fetchUrl}</div>
556
+ <div class="text-3xs text-slate-400 truncate font-mono">{remote.fetchUrl}</div>
447
557
  </div>
448
558
  {#if remote.name === remoteName}
449
559
  <Icon name="lucide:check" class="w-3.5 h-3.5 text-violet-600 shrink-0" />
@@ -501,5 +611,6 @@
501
611
  </button>
502
612
  {/if}
503
613
  </div>
614
+
504
615
  </div>
505
616
  </header>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { fade, scale } from 'svelte/transition';
2
+ import { scale } from 'svelte/transition';
3
3
  import { cubicOut } from 'svelte/easing';
4
4
  import Icon from '$frontend/lib/components/common/Icon.svelte';
5
5
  import LayoutPreview from '$frontend/lib/components/settings/appearance/LayoutPreview.svelte';
@@ -9,7 +9,6 @@
9
9
  applyLayoutPreset,
10
10
  type LayoutPreset
11
11
  } from '$frontend/lib/stores/ui/workspace.svelte';
12
- import { settings } from '$frontend/lib/stores/features/settings.svelte';
13
12
  import { clickOutside } from '$frontend/lib/utils/click-outside';
14
13
  import type { IconName } from '$shared/types/ui/icons';
15
14
 
@@ -27,36 +26,29 @@
27
26
  const presetCategories = [
28
27
  {
29
28
  name: 'Single Panel',
30
- presets: builtInPresets.slice(0, 5)
29
+ presets: builtInPresets.slice(0, 1)
31
30
  },
32
31
  {
33
32
  name: 'Two Panels',
34
- presets: builtInPresets.slice(5, 13)
33
+ presets: builtInPresets.slice(1, 4)
35
34
  },
36
35
  {
37
36
  name: 'Three Panels',
38
- presets: builtInPresets.slice(13, 22)
37
+ presets: builtInPresets.slice(4, 8)
39
38
  },
40
39
  {
41
40
  name: 'Four Panels',
42
- presets: builtInPresets.slice(22, 26)
41
+ presets: builtInPresets.slice(8, 11)
43
42
  },
44
43
  {
45
44
  name: 'Five Panels',
46
- presets: builtInPresets.slice(26, 28)
45
+ presets: builtInPresets.slice(11, 12)
47
46
  }
48
47
  ];
49
48
 
50
49
  // Filter visible presets and categories
51
50
  const visibleCategories = $derived(
52
- presetCategories
53
- .map((category) => ({
54
- ...category,
55
- presets: category.presets.filter(
56
- (preset) => settings.layoutPresetVisibility[preset.id] !== false
57
- )
58
- }))
59
- .filter((category) => category.presets.length > 0)
51
+ presetCategories.filter((category) => category.presets.length > 0)
60
52
  );
61
53
 
62
54
  function toggleMenu() {
@@ -129,19 +121,11 @@
129
121
  {#each category.presets as preset}
130
122
  <button
131
123
  type="button"
132
- class="flex items-center p-2.5 bg-transparent border border-slate-200 dark:border-slate-800 rounded-lg text-slate-700 dark:text-slate-300 text-sm text-left cursor-pointer transition-all duration-150 hover:bg-violet-500/10 hover:border-violet-500/20 {workspaceState.activePresetId ===
133
- preset.id
134
- ? 'bg-violet-500/5 border-violet-500/30'
135
- : ''}"
124
+ class="flex items-center p-2.5 bg-transparent border border-slate-200 dark:border-slate-800 rounded-lg text-slate-700 dark:text-slate-300 text-sm text-left cursor-pointer transition-all duration-150 hover:bg-violet-500/10 hover:border-violet-500/20"
136
125
  onclick={() => handleApplyPreset(preset)}
137
126
  >
138
127
  <div class="flex flex-col gap-1 flex-1 min-w-0">
139
- <div class="flex justify-between">
140
- <span class="font-medium text-xs">{preset.name}</span>
141
- {#if workspaceState.activePresetId === preset.id}
142
- <Icon name="lucide:check" class="w-3.5 h-3.5 text-violet-600 dark:text-violet-400 shrink-0" />
143
- {/if}
144
- </div>
128
+ <span class="font-medium text-xs">{preset.name}</span>
145
129
  <!-- Visual Preview -->
146
130
  <div class="w-full">
147
131
  <LayoutPreview layout={preset.layout} size="small" />
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import type { SplitNode } from '$frontend/lib/stores/ui/workspace.svelte';
2
+ import { type SplitNode, setPanelAtPath, closePanelAtPath, PANEL_OPTIONS } from '$frontend/lib/stores/ui/workspace.svelte';
3
+ import Icon from '$frontend/lib/components/common/Icon.svelte';
3
4
  import PanelContainer from '../../PanelContainer.svelte';
4
5
  import Container from './Container.svelte';
5
6
 
@@ -18,11 +19,35 @@
18
19
  <PanelContainer panelId={node.panelId} />
19
20
  </div>
20
21
  {:else}
21
- <!-- Empty slot -->
22
+ <!-- Empty slot: Panel picker -->
22
23
  <div
23
- class="split-pane-empty flex items-center justify-center h-full w-full bg-slate-100 dark:bg-slate-900/50 border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-lg"
24
+ class="split-pane-empty flex flex-col items-center justify-center gap-4 h-full w-full bg-white/90 dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden"
24
25
  >
25
- <span class="text-sm text-slate-400 dark:text-slate-500">Empty Panel</span>
26
+ <span class="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Choose Panel</span>
27
+ <div class="grid grid-cols-3 gap-2">
28
+ {#each PANEL_OPTIONS as option}
29
+ <button
30
+ type="button"
31
+ class="flex flex-col items-center justify-center gap-2 w-26 h-18 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl text-xs font-medium text-slate-600 dark:text-slate-300 cursor-pointer transition-all duration-150 hover:bg-violet-500/10 hover:border-violet-400 hover:text-violet-600 dark:hover:text-violet-400"
32
+ onclick={() => setPanelAtPath(path, option.id)}
33
+ >
34
+ <Icon name={option.icon} class="w-5 h-5" />
35
+ <span>{option.title}</span>
36
+ </button>
37
+ {/each}
38
+ <!-- Close / Cancel button -->
39
+ {#if path.length > 0}
40
+ <button
41
+ type="button"
42
+ class="flex flex-col items-center justify-center gap-2 w-26 h-18 bg-transparent border border-dashed border-slate-300 dark:border-slate-700 rounded-xl text-xs font-medium text-slate-400 dark:text-slate-500 cursor-pointer transition-all duration-150 hover:border-red-400 hover:text-red-500 dark:hover:text-red-400"
43
+ onclick={() => closePanelAtPath(path)}
44
+ title="Remove this panel slot"
45
+ >
46
+ <Icon name="lucide:x" class="w-5 h-5" />
47
+ <span>Cancel</span>
48
+ </button>
49
+ {/if}
50
+ </div>
26
51
  </div>
27
52
  {/if}
28
53
  {:else if node.type === 'split'}
@@ -1,86 +1,77 @@
1
- /**
2
- * Global Stream Monitor Service
3
- *
4
- * Single source of truth for chat stream notifications (sound + push).
5
- *
6
- * Listens for chat:stream-finished events from the backend and triggers
7
- * notifications for ALL projects — both active and non-active.
8
- *
9
- * The backend uses ws.emit.projectMembers() to send this event to all
10
- * users who have been associated with the project, even if they switched
11
- * to a different project.
12
- */
13
-
14
- import { soundNotification, pushNotification } from '$frontend/lib/services/notification';
15
- import { projectState } from '$frontend/lib/stores/core/projects.svelte';
16
- import { sessionState } from '$frontend/lib/stores/core/sessions.svelte';
17
- import { debug } from '$shared/utils/logger';
18
- import ws from '$frontend/lib/utils/ws';
19
-
20
- class GlobalStreamMonitor {
21
- private initialized = false;
22
-
23
- /**
24
- * Initialize the monitor - subscribes to the WS event.
25
- * Safe to call multiple times (idempotent).
26
- *
27
- * Only triggers sound/push when the finished stream belongs to the
28
- * user's current project AND current chat session.
29
- */
30
- initialize(): void {
31
- if (this.initialized) return;
32
- this.initialized = true;
33
-
34
- debug.log('notification', 'GlobalStreamMonitor: Initializing WS listener');
35
-
36
- ws.on('chat:stream-finished', async (data) => {
37
- const { projectId, chatSessionId, status, timestamp } = data;
38
- const isActiveProject = projectState.currentProject?.id === projectId;
39
- const isActiveSession = sessionState.currentSession?.id === chatSessionId;
40
-
41
- debug.log('notification', 'GlobalStreamMonitor: Stream finished', {
42
- projectId,
43
- chatSessionId,
44
- status,
45
- timestamp,
46
- isActiveProject,
47
- isActiveSession
48
- });
49
-
50
- // Only notify if the stream is for the user's current project AND session
51
- if (!isActiveProject || !isActiveSession) return;
52
-
53
- // Play sound notification
54
- try {
55
- await soundNotification.play();
56
- } catch (error) {
57
- debug.error('notification', 'Error playing sound notification:', error);
58
- }
59
-
60
- // Send push notification with project context
61
- try {
62
- const projectName = projectState.projects.find(p => p.id === projectId)?.name || 'Unknown';
63
-
64
- if (status === 'completed') {
65
- await pushNotification.sendChatComplete(`Chat response ready in "${projectName}"`);
66
- } else if (status === 'error') {
67
- await pushNotification.sendChatError(`Chat error in "${projectName}"`);
68
- } else if (status === 'cancelled') {
69
- await pushNotification.sendChatComplete(`Chat interrupted in "${projectName}"`);
70
- }
71
- } catch (error) {
72
- debug.error('notification', 'Error sending push notification:', error);
73
- }
74
- });
75
- }
76
-
77
- /**
78
- * Clear state (for cleanup/testing)
79
- */
80
- clear(): void {
81
- debug.log('notification', 'GlobalStreamMonitor: Clearing state');
82
- }
83
- }
84
-
85
- // Export singleton instance
86
- export const globalStreamMonitor = new GlobalStreamMonitor();
1
+ /**
2
+ * Global Stream Monitor Service
3
+ *
4
+ * Single source of truth for chat stream notifications (sound + push).
5
+ *
6
+ * Listens for chat:stream-finished events from the backend and triggers
7
+ * notifications for ALL projects — both active and non-active.
8
+ *
9
+ * The backend uses ws.emit.projectMembers() to send this event to all
10
+ * users who have been associated with the project, even if they switched
11
+ * to a different project.
12
+ */
13
+
14
+ import { soundNotification, pushNotification } from '$frontend/lib/services/notification';
15
+ import { projectState } from '$frontend/lib/stores/core/projects.svelte';
16
+ import { debug } from '$shared/utils/logger';
17
+ import ws from '$frontend/lib/utils/ws';
18
+
19
+ class GlobalStreamMonitor {
20
+ private initialized = false;
21
+
22
+ /**
23
+ * Initialize the monitor - subscribes to the WS event.
24
+ * Safe to call multiple times (idempotent).
25
+ *
26
+ * Triggers sound/push for ALL projects the user is a member of,
27
+ * not just the active one background streams deserve notifications too.
28
+ */
29
+ initialize(): void {
30
+ if (this.initialized) return;
31
+ this.initialized = true;
32
+
33
+ debug.log('notification', 'GlobalStreamMonitor: Initializing WS listener');
34
+
35
+ ws.on('chat:stream-finished', async (data) => {
36
+ const { projectId, status, timestamp } = data;
37
+
38
+ debug.log('notification', 'GlobalStreamMonitor: Stream finished', {
39
+ projectId,
40
+ status,
41
+ timestamp
42
+ });
43
+
44
+ // Play sound notification
45
+ try {
46
+ await soundNotification.play();
47
+ } catch (error) {
48
+ debug.error('notification', 'Error playing sound notification:', error);
49
+ }
50
+
51
+ // Send push notification with project context
52
+ try {
53
+ const projectName = projectState.projects.find(p => p.id === projectId)?.name || 'Unknown';
54
+
55
+ if (status === 'completed') {
56
+ await pushNotification.sendChatComplete(`Chat response ready in "${projectName}"`);
57
+ } else if (status === 'error') {
58
+ await pushNotification.sendChatError(`Chat error in "${projectName}"`);
59
+ } else if (status === 'cancelled') {
60
+ await pushNotification.sendChatComplete(`Chat interrupted in "${projectName}"`);
61
+ }
62
+ } catch (error) {
63
+ debug.error('notification', 'Error sending push notification:', error);
64
+ }
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Clear state (for cleanup/testing)
70
+ */
71
+ clear(): void {
72
+ debug.log('notification', 'GlobalStreamMonitor: Clearing state');
73
+ }
74
+ }
75
+
76
+ // Export singleton instance
77
+ export const globalStreamMonitor = new GlobalStreamMonitor();