@myrialabs/clopen 0.2.5 → 0.2.7
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/backend/chat/stream-manager.ts +136 -10
- package/backend/database/queries/session-queries.ts +9 -0
- package/backend/engine/adapters/claude/error-handler.ts +7 -2
- package/backend/engine/adapters/claude/stream.ts +16 -7
- package/backend/index.ts +25 -3
- package/backend/mcp/servers/browser-automation/browser.ts +23 -6
- package/backend/preview/browser/browser-mcp-control.ts +32 -16
- package/backend/preview/browser/browser-pool.ts +3 -1
- package/backend/preview/browser/browser-preview-service.ts +16 -17
- package/backend/preview/browser/browser-tab-manager.ts +1 -1
- package/backend/preview/browser/browser-video-capture.ts +199 -156
- package/backend/preview/browser/scripts/audio-stream.ts +11 -0
- package/backend/preview/browser/scripts/video-stream.ts +3 -5
- package/backend/snapshot/helpers.ts +15 -2
- package/backend/ws/chat/stream.ts +1 -1
- package/backend/ws/preview/browser/tab-info.ts +5 -2
- package/backend/ws/snapshot/restore.ts +43 -2
- package/frontend/components/chat/input/ChatInput.svelte +6 -4
- package/frontend/components/chat/input/components/ChatInputActions.svelte +10 -0
- package/frontend/components/chat/input/composables/use-chat-actions.svelte.ts +6 -2
- package/frontend/components/chat/message/MessageBubble.svelte +22 -1
- package/frontend/components/chat/tools/components/TerminalCommand.svelte +2 -2
- package/frontend/components/files/FileViewer.svelte +13 -2
- package/frontend/components/history/HistoryModal.svelte +1 -1
- package/frontend/components/preview/browser/BrowserPreview.svelte +15 -0
- package/frontend/components/preview/browser/components/Canvas.svelte +432 -69
- package/frontend/components/preview/browser/components/Container.svelte +23 -1
- package/frontend/components/preview/browser/components/Toolbar.svelte +1 -8
- package/frontend/components/preview/browser/core/coordinator.svelte.ts +27 -4
- package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +36 -3
- package/frontend/components/preview/browser/core/tab-operations.svelte.ts +1 -0
- package/frontend/components/terminal/TerminalTabs.svelte +1 -2
- package/frontend/components/workspace/PanelHeader.svelte +15 -0
- package/frontend/components/workspace/panels/FilesPanel.svelte +1 -0
- package/frontend/components/workspace/panels/GitPanel.svelte +27 -13
- package/frontend/components/workspace/panels/PreviewPanel.svelte +2 -0
- package/frontend/services/chat/chat.service.ts +9 -8
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +43 -138
- package/frontend/stores/core/app.svelte.ts +4 -3
- package/frontend/stores/core/presence.svelte.ts +3 -2
- package/frontend/stores/core/sessions.svelte.ts +2 -0
- package/frontend/stores/ui/notification.svelte.ts +4 -1
- package/package.json +1 -1
|
@@ -44,18 +44,28 @@
|
|
|
44
44
|
// Preview dimensions (bindable to parent)
|
|
45
45
|
previewDimensions = $bindable<any>({ scale: 1 }),
|
|
46
46
|
|
|
47
|
+
// Touch interaction mode
|
|
48
|
+
touchMode = $bindable<'scroll' | 'cursor'>('scroll'),
|
|
49
|
+
|
|
47
50
|
// Callbacks
|
|
48
51
|
onInteraction = $bindable<(action: any) => void>(() => {}),
|
|
49
52
|
onRetry = $bindable<() => void>(() => {})
|
|
50
53
|
} = $props();
|
|
51
54
|
|
|
52
55
|
let previewContainer = $state<HTMLDivElement | undefined>();
|
|
56
|
+
let touchCursorPos = $state<{ x: number; y: number; visible: boolean; clicking?: boolean }>({ x: 0, y: 0, visible: false });
|
|
53
57
|
|
|
54
58
|
// Solid loading overlay: shown during initial load states
|
|
59
|
+
// Skip when lastFrameData exists (tab was previously loaded - snapshot handles display)
|
|
55
60
|
const showSolidOverlay = $derived(
|
|
56
|
-
isLaunchingBrowser || !sessionInfo || (!isStreamReady && !isNavigating && !isReconnecting)
|
|
61
|
+
isLaunchingBrowser || !sessionInfo || (!isStreamReady && !isNavigating && !isReconnecting && !lastFrameData)
|
|
57
62
|
);
|
|
58
63
|
|
|
64
|
+
// Diagnostic: log whenever showSolidOverlay changes
|
|
65
|
+
$effect(() => {
|
|
66
|
+
debug.log('preview', `[DIAG] showSolidOverlay=${showSolidOverlay} (isLaunchingBrowser=${isLaunchingBrowser}, sessionInfo=${!!sessionInfo}, isStreamReady=${isStreamReady}, isNavigating=${isNavigating}, isReconnecting=${isReconnecting}, lastFrameData=${!!lastFrameData})`);
|
|
67
|
+
});
|
|
68
|
+
|
|
59
69
|
// Navigation overlay state with debounce to prevent flickering during state transitions
|
|
60
70
|
let showNavigationOverlay = $state(false);
|
|
61
71
|
let overlayHideTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -88,6 +98,10 @@
|
|
|
88
98
|
}
|
|
89
99
|
});
|
|
90
100
|
|
|
101
|
+
function handleTouchCursorUpdate(pos: { x: number; y: number; visible: boolean; clicking?: boolean }) {
|
|
102
|
+
touchCursorPos = { x: pos.x, y: pos.y, visible: pos.visible, clicking: pos.clicking };
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
onDestroy(() => {
|
|
92
106
|
if (overlayHideTimeout) {
|
|
93
107
|
clearTimeout(overlayHideTimeout);
|
|
@@ -370,10 +384,12 @@
|
|
|
370
384
|
bind:isStreamReady
|
|
371
385
|
bind:isNavigating
|
|
372
386
|
bind:isReconnecting
|
|
387
|
+
bind:touchMode
|
|
373
388
|
onInteraction={handleCanvasInteraction}
|
|
374
389
|
onCursorUpdate={handleCursorUpdate}
|
|
375
390
|
onFrameUpdate={handleFrameUpdate}
|
|
376
391
|
onRequestScreencastRefresh={handleScreencastRefresh}
|
|
392
|
+
onTouchCursorUpdate={handleTouchCursorUpdate}
|
|
377
393
|
/>
|
|
378
394
|
</div>
|
|
379
395
|
{/if}
|
|
@@ -426,6 +442,7 @@
|
|
|
426
442
|
</div>
|
|
427
443
|
{/if}
|
|
428
444
|
|
|
445
|
+
|
|
429
446
|
</div>
|
|
430
447
|
{:else}
|
|
431
448
|
<div
|
|
@@ -444,6 +461,11 @@
|
|
|
444
461
|
<VirtualCursor cursor={virtualCursor} />
|
|
445
462
|
{/if}
|
|
446
463
|
|
|
464
|
+
<!-- Touch Cursor - shown in cursor simulation mode -->
|
|
465
|
+
{#if touchMode === 'cursor' && touchCursorPos.visible}
|
|
466
|
+
<VirtualCursor cursor={touchCursorPos} />
|
|
467
|
+
{/if}
|
|
468
|
+
|
|
447
469
|
<!-- MCP Virtual Cursor -->
|
|
448
470
|
{#if mcpVirtualCursor.visible}
|
|
449
471
|
<VirtualCursor cursor={mcpVirtualCursor} />
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
{@const isActive = tab.id === activeTabId}
|
|
223
223
|
<button
|
|
224
224
|
type="button"
|
|
225
|
-
class="group relative flex items-center justify-center gap-1
|
|
225
|
+
class="group relative flex items-center justify-center gap-1 pr-2 pl-3 py-2 text-xs font-medium transition-colors min-w-0 max-w-xs cursor-pointer
|
|
226
226
|
{isActive
|
|
227
227
|
? 'text-violet-600 dark:text-violet-400'
|
|
228
228
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
@@ -230,13 +230,6 @@
|
|
|
230
230
|
role="tab"
|
|
231
231
|
tabindex="0"
|
|
232
232
|
>
|
|
233
|
-
{#if tab.id === mcpControlledTabId}
|
|
234
|
-
<Icon name="lucide:bot" class="w-3 h-3 flex-shrink-0 text-amber-500" />
|
|
235
|
-
{:else if tab.isLoading}
|
|
236
|
-
<Icon name="lucide:loader-circle" class="w-3 h-3 animate-spin flex-shrink-0" />
|
|
237
|
-
{:else}
|
|
238
|
-
<Icon name="lucide:globe" class="w-3 h-3 flex-shrink-0" />
|
|
239
|
-
{/if}
|
|
240
233
|
<span class="truncate max-w-28" title={tab.url}>
|
|
241
234
|
{tab.title || 'New Tab'}
|
|
242
235
|
</span>
|
|
@@ -486,7 +486,10 @@ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
|
|
|
486
486
|
|
|
487
487
|
if (!existingTabs || existingTabs.count === 0) {
|
|
488
488
|
debug.log('preview', '📭 No existing sessions to recover');
|
|
489
|
-
|
|
489
|
+
// Notify parent about 0-tab recovery (enables empty tab creation)
|
|
490
|
+
if (onSessionsRecovered) {
|
|
491
|
+
onSessionsRecovered(0);
|
|
492
|
+
}
|
|
490
493
|
return;
|
|
491
494
|
}
|
|
492
495
|
|
|
@@ -507,6 +510,13 @@ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
|
|
|
507
510
|
if (backendTab.isActive && frontendId) {
|
|
508
511
|
activeTabFrontendId = frontendId;
|
|
509
512
|
}
|
|
513
|
+
|
|
514
|
+
// Restore MCP control state if this tab was being controlled
|
|
515
|
+
if (backendTab.isMcpControlled && frontendId) {
|
|
516
|
+
debug.log('preview', `🎮 Restoring MCP control state for recovered tab: ${frontendId} (session: ${backendTab.tabId})`);
|
|
517
|
+
mcpHandler.restoreControlState(frontendId, backendTab.tabId);
|
|
518
|
+
}
|
|
519
|
+
|
|
510
520
|
totalRestored++;
|
|
511
521
|
}
|
|
512
522
|
|
|
@@ -533,6 +543,11 @@ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
|
|
|
533
543
|
|
|
534
544
|
debug.log('preview', `✅ Session recovery complete - restored ${totalRestored} tabs`);
|
|
535
545
|
|
|
546
|
+
// Diagnostic: dump state of all restored tabs
|
|
547
|
+
for (const tab of tabManager.getAllTabs()) {
|
|
548
|
+
debug.log('preview', `[DIAG] Restored tab: id=${tab.id}, sessionId=${tab.sessionId}, url=${tab.url}, isConnected=${tab.isConnected}, isStreamReady=${tab.isStreamReady}, isLaunchingBrowser=${tab.isLaunchingBrowser}, sessionInfo=${!!tab.sessionInfo}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
536
551
|
// Notify parent that sessions were recovered
|
|
537
552
|
if (onSessionsRecovered) {
|
|
538
553
|
onSessionsRecovered(totalRestored);
|
|
@@ -781,7 +796,7 @@ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
|
|
|
781
796
|
const newProjectId = getProjectId();
|
|
782
797
|
debug.log('preview', `🔄 Switching to project: ${newProjectId}`);
|
|
783
798
|
|
|
784
|
-
// Set restore lock during project switch
|
|
799
|
+
// Set restore lock during entire project switch (prevents race conditions)
|
|
785
800
|
isRestoring = true;
|
|
786
801
|
|
|
787
802
|
try {
|
|
@@ -794,14 +809,22 @@ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
|
|
|
794
809
|
// Clear frontend tracking
|
|
795
810
|
browserCleanup.clearAll();
|
|
796
811
|
|
|
812
|
+
// Reset MCP control state to avoid stale tab IDs after tab recreation
|
|
813
|
+
mcpHandler.resetControlState();
|
|
814
|
+
|
|
797
815
|
// Recover sessions from new project
|
|
798
|
-
// Note: recoverExistingSessions will handle its own lock
|
|
799
|
-
isRestoring = false;
|
|
800
816
|
await recoverExistingSessions();
|
|
801
817
|
} catch (error) {
|
|
802
818
|
debug.error('preview', '❌ Error switching project:', error);
|
|
819
|
+
} finally {
|
|
803
820
|
isRestoring = false;
|
|
804
821
|
}
|
|
822
|
+
|
|
823
|
+
// After recovery and lock release, create empty tab if no tabs exist
|
|
824
|
+
if (tabManager.getAllTabs().length === 0) {
|
|
825
|
+
debug.log('preview', '📭 No tabs after project switch, creating empty tab');
|
|
826
|
+
createNewTab('');
|
|
827
|
+
}
|
|
805
828
|
}
|
|
806
829
|
|
|
807
830
|
return {
|
|
@@ -145,8 +145,8 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function handleCursorPosition(data: { sessionId: string; x: number; y: number; timestamp: number; source: 'mcp' }) {
|
|
148
|
-
// Only
|
|
149
|
-
if (mcpControlState.isControlled && mcpControlState.browserSessionId === data.sessionId && transformBrowserToDisplayCoordinates) {
|
|
148
|
+
// Only show cursor if MCP is controlling AND user is currently viewing that tab
|
|
149
|
+
if (mcpControlState.isControlled && mcpControlState.browserSessionId === data.sessionId && isCurrentTabMcpControlled() && transformBrowserToDisplayCoordinates) {
|
|
150
150
|
const transformedPosition = transformBrowserToDisplayCoordinates(data.x, data.y);
|
|
151
151
|
if (transformedPosition && onCursorUpdate) {
|
|
152
152
|
onCursorUpdate(transformedPosition.x, transformedPosition.y, false);
|
|
@@ -155,7 +155,8 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function handleCursorClick(data: { sessionId: string; x: number; y: number; timestamp: number; source: 'mcp' }) {
|
|
158
|
-
if
|
|
158
|
+
// Only show cursor click if MCP is controlling AND user is currently viewing that tab
|
|
159
|
+
if (mcpControlState.isControlled && mcpControlState.browserSessionId === data.sessionId && isCurrentTabMcpControlled() && transformBrowserToDisplayCoordinates) {
|
|
159
160
|
const transformedPosition = transformBrowserToDisplayCoordinates(data.x, data.y);
|
|
160
161
|
if (transformedPosition && onCursorUpdate) {
|
|
161
162
|
onCursorUpdate(transformedPosition.x, transformedPosition.y, true);
|
|
@@ -163,6 +164,36 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Restore MCP control state after session recovery (browser refresh or project switch)
|
|
169
|
+
* Called when recovered backend tab was previously MCP-controlled
|
|
170
|
+
*/
|
|
171
|
+
function restoreControlState(frontendTabId: string, browserSessionId: string): void {
|
|
172
|
+
debug.log('preview', `🔄 Restoring MCP control state for tab: ${frontendTabId} (session: ${browserSessionId})`);
|
|
173
|
+
mcpControlState = {
|
|
174
|
+
isControlled: true,
|
|
175
|
+
controlledTabId: frontendTabId,
|
|
176
|
+
browserSessionId: browserSessionId,
|
|
177
|
+
startedAt: Date.now()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Reset MCP control state (called before project switch recovery)
|
|
183
|
+
*/
|
|
184
|
+
function resetControlState(): void {
|
|
185
|
+
debug.log('preview', `🔄 Resetting MCP control state`);
|
|
186
|
+
mcpControlState = {
|
|
187
|
+
isControlled: false,
|
|
188
|
+
controlledTabId: null,
|
|
189
|
+
browserSessionId: null,
|
|
190
|
+
startedAt: null
|
|
191
|
+
};
|
|
192
|
+
if (onCursorHide) {
|
|
193
|
+
onCursorHide();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
166
197
|
function handleTestCompleted(_data: { sessionId: string; timestamp: number; source: 'mcp' }) {
|
|
167
198
|
// Cursor is hidden via chat:complete / chat:cancelled listeners instead,
|
|
168
199
|
// because test-completed fires per-tool-call, not at end of full request.
|
|
@@ -297,6 +328,8 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
297
328
|
setupEventListeners,
|
|
298
329
|
isCurrentTabMcpControlled,
|
|
299
330
|
getControlState,
|
|
331
|
+
restoreControlState,
|
|
332
|
+
resetControlState,
|
|
300
333
|
get mcpControlState() { return mcpControlState; }
|
|
301
334
|
};
|
|
302
335
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
{@const isActive = session.isActive}
|
|
38
38
|
<button
|
|
39
39
|
type="button"
|
|
40
|
-
class="group relative flex items-center justify-center gap-1
|
|
40
|
+
class="group relative flex items-center justify-center gap-1 pr-2 pl-3 py-2 text-xs font-medium transition-colors min-w-0 max-w-xs cursor-pointer
|
|
41
41
|
{isActive
|
|
42
42
|
? 'text-violet-600 dark:text-violet-400'
|
|
43
43
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
role="tab"
|
|
46
46
|
tabindex="0"
|
|
47
47
|
>
|
|
48
|
-
<Icon name="lucide:terminal" class="w-3 h-3 flex-shrink-0" />
|
|
49
48
|
<span class="truncate max-w-28">{session.name}</span>
|
|
50
49
|
<!-- Close button -->
|
|
51
50
|
<span
|
|
@@ -481,6 +481,21 @@
|
|
|
481
481
|
{/if}
|
|
482
482
|
</div>
|
|
483
483
|
|
|
484
|
+
<!-- Touch mode toggle (scroll ↔ trackpad cursor) -->
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
class="flex items-center justify-center gap-1.5 {isMobile ? 'px-2 h-9' : 'px-1 h-6'} bg-transparent border-none rounded-md cursor-pointer transition-all duration-150 hover:bg-violet-500/10
|
|
488
|
+
{previewPanelRef?.panelActions?.getTouchMode() === 'cursor' ? 'text-violet-600 dark:text-violet-400' : 'text-slate-500 hover:text-slate-900 dark:hover:text-slate-100'}"
|
|
489
|
+
onclick={() => {
|
|
490
|
+
const current = previewPanelRef?.panelActions?.getTouchMode() || 'scroll';
|
|
491
|
+
previewPanelRef?.panelActions?.setTouchMode(current === 'scroll' ? 'cursor' : 'scroll');
|
|
492
|
+
}}
|
|
493
|
+
title={previewPanelRef?.panelActions?.getTouchMode() === 'cursor' ? 'Trackpad mode: 1-finger moves cursor, tap=click, 2-finger scroll/right-click' : 'Scroll mode: touch scrolls the page (tap to click)'}
|
|
494
|
+
>
|
|
495
|
+
<Icon name={previewPanelRef?.panelActions?.getTouchMode() === 'cursor' ? 'lucide:mouse-pointer-2' : 'lucide:pointer'} class={isMobile ? 'w-4.5 h-4.5' : 'w-4 h-4'} />
|
|
496
|
+
<span class="text-xs font-medium">{previewPanelRef?.panelActions?.getTouchMode() === 'cursor' ? 'Cursor' : 'Touch'}</span>
|
|
497
|
+
</button>
|
|
498
|
+
|
|
484
499
|
<!-- Rotation toggle -->
|
|
485
500
|
<button
|
|
486
501
|
type="button"
|
|
@@ -942,6 +942,16 @@
|
|
|
942
942
|
}
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
+
async function copyTagHash(hash: string, e: MouseEvent) {
|
|
946
|
+
e.stopPropagation();
|
|
947
|
+
try {
|
|
948
|
+
await navigator.clipboard.writeText(hash);
|
|
949
|
+
showInfo('Copied', `Hash ${hash.substring(0, 7)} copied to clipboard`);
|
|
950
|
+
} catch {
|
|
951
|
+
showError('Copy Failed', 'Could not copy to clipboard');
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
945
955
|
// ============================
|
|
946
956
|
// Lifecycle
|
|
947
957
|
// ============================
|
|
@@ -1323,8 +1333,8 @@
|
|
|
1323
1333
|
<div class="group flex items-center gap-2 px-2.5 py-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors">
|
|
1324
1334
|
<Icon name="lucide:archive" class="w-4 h-4 text-slate-400 shrink-0" />
|
|
1325
1335
|
<div class="flex-1 min-w-0">
|
|
1326
|
-
<p class="text-
|
|
1327
|
-
<p class="text-
|
|
1336
|
+
<p class="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{entry.message}</p>
|
|
1337
|
+
<p class="text-xs text-slate-400 dark:text-slate-500">stash@{{entry.index}}</p>
|
|
1328
1338
|
</div>
|
|
1329
1339
|
<div class="flex items-center gap-0.5 shrink-0">
|
|
1330
1340
|
<button
|
|
@@ -1416,21 +1426,25 @@
|
|
|
1416
1426
|
<div class="space-y-1 px-1">
|
|
1417
1427
|
{#each tags as tag (tag.name)}
|
|
1418
1428
|
<div class="group flex items-center gap-2 px-2.5 py-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors">
|
|
1419
|
-
<
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1429
|
+
<span title={tag.isAnnotated ? 'Annotated tag' : 'Lightweight tag'} class="shrink-0">
|
|
1430
|
+
<Icon
|
|
1431
|
+
name={tag.isAnnotated ? 'lucide:bookmark' : 'lucide:tag'}
|
|
1432
|
+
class="w-4 h-4 {tag.isAnnotated ? 'text-amber-500' : 'text-slate-400'}"
|
|
1433
|
+
/>
|
|
1434
|
+
</span>
|
|
1423
1435
|
<div class="flex-1 min-w-0">
|
|
1436
|
+
<p class="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{tag.name}</p>
|
|
1424
1437
|
<div class="flex items-center gap-1.5">
|
|
1425
|
-
<
|
|
1426
|
-
|
|
1427
|
-
|
|
1438
|
+
<button
|
|
1439
|
+
type="button"
|
|
1440
|
+
class="text-xs font-mono text-slate-400 dark:text-slate-500 hover:text-violet-600 dark:hover:text-violet-400 bg-transparent border-none cursor-pointer p-0 shrink-0 transition-colors"
|
|
1441
|
+
onclick={(e) => copyTagHash(tag.hash, e)}
|
|
1442
|
+
title="Copy tag hash"
|
|
1443
|
+
>{tag.hash.slice(0, 7)}</button>
|
|
1444
|
+
{#if tag.message}
|
|
1445
|
+
<span class="text-xs text-slate-400 dark:text-slate-500 truncate">{tag.message}</span>
|
|
1428
1446
|
{/if}
|
|
1429
1447
|
</div>
|
|
1430
|
-
{#if tag.message}
|
|
1431
|
-
<p class="text-3xs text-slate-500 dark:text-slate-400 truncate">{tag.message}</p>
|
|
1432
|
-
{/if}
|
|
1433
|
-
<p class="text-3xs text-slate-400 dark:text-slate-500 font-mono">{tag.hash}</p>
|
|
1434
1448
|
</div>
|
|
1435
1449
|
<div class="flex items-center gap-0.5 shrink-0">
|
|
1436
1450
|
<button
|
|
@@ -104,6 +104,8 @@
|
|
|
104
104
|
|
|
105
105
|
// Export actions for DesktopPanel header
|
|
106
106
|
export const panelActions = {
|
|
107
|
+
getTouchMode: () => browserPreviewRef?.browserActions?.getTouchMode() || 'scroll',
|
|
108
|
+
setTouchMode: (mode: 'scroll' | 'cursor') => { browserPreviewRef?.browserActions?.setTouchMode(mode); },
|
|
107
109
|
getDeviceSize: () => deviceSize,
|
|
108
110
|
getRotation: () => rotation,
|
|
109
111
|
getScale: () => previewDimensions?.scale || 1,
|
|
@@ -196,7 +196,12 @@ class ChatService {
|
|
|
196
196
|
this.streamCompleted = true;
|
|
197
197
|
this.reconnected = false;
|
|
198
198
|
this.activeProcessId = null;
|
|
199
|
-
|
|
199
|
+
// Don't clear isCancelling here — it causes a race with presence.
|
|
200
|
+
// The chat:cancelled WS event arrives before broadcastPresence() updates,
|
|
201
|
+
// so clearing isCancelling lets the presence $effect re-enable isLoading
|
|
202
|
+
// (because hasActiveForSession is still true). The presence $effect will
|
|
203
|
+
// clear isCancelling once the stream is actually gone from presence.
|
|
204
|
+
this.setProcessState({ isLoading: false, isWaitingInput: false });
|
|
200
205
|
|
|
201
206
|
// Mark any tool_use blocks that never got a tool_result
|
|
202
207
|
this.markInterruptedTools();
|
|
@@ -480,13 +485,9 @@ class ChatService {
|
|
|
480
485
|
// before a stale non-reasoning stream_event instead of at the end).
|
|
481
486
|
this.cleanupStreamEvents();
|
|
482
487
|
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
if (appState.isCancelling) {
|
|
487
|
-
appState.isCancelling = false;
|
|
488
|
-
}
|
|
489
|
-
}, 10000);
|
|
488
|
+
// No safety timeout needed — cancel completion is confirmed via WS events:
|
|
489
|
+
// chat:cancelled clears isLoading, then presence update clears isCancelling.
|
|
490
|
+
// If WS disconnects, reconnection logic re-fetches presence and clears state.
|
|
490
491
|
}
|
|
491
492
|
|
|
492
493
|
/**
|