@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.
Files changed (43) hide show
  1. package/backend/chat/stream-manager.ts +136 -10
  2. package/backend/database/queries/session-queries.ts +9 -0
  3. package/backend/engine/adapters/claude/error-handler.ts +7 -2
  4. package/backend/engine/adapters/claude/stream.ts +16 -7
  5. package/backend/index.ts +25 -3
  6. package/backend/mcp/servers/browser-automation/browser.ts +23 -6
  7. package/backend/preview/browser/browser-mcp-control.ts +32 -16
  8. package/backend/preview/browser/browser-pool.ts +3 -1
  9. package/backend/preview/browser/browser-preview-service.ts +16 -17
  10. package/backend/preview/browser/browser-tab-manager.ts +1 -1
  11. package/backend/preview/browser/browser-video-capture.ts +199 -156
  12. package/backend/preview/browser/scripts/audio-stream.ts +11 -0
  13. package/backend/preview/browser/scripts/video-stream.ts +3 -5
  14. package/backend/snapshot/helpers.ts +15 -2
  15. package/backend/ws/chat/stream.ts +1 -1
  16. package/backend/ws/preview/browser/tab-info.ts +5 -2
  17. package/backend/ws/snapshot/restore.ts +43 -2
  18. package/frontend/components/chat/input/ChatInput.svelte +6 -4
  19. package/frontend/components/chat/input/components/ChatInputActions.svelte +10 -0
  20. package/frontend/components/chat/input/composables/use-chat-actions.svelte.ts +6 -2
  21. package/frontend/components/chat/message/MessageBubble.svelte +22 -1
  22. package/frontend/components/chat/tools/components/TerminalCommand.svelte +2 -2
  23. package/frontend/components/files/FileViewer.svelte +13 -2
  24. package/frontend/components/history/HistoryModal.svelte +1 -1
  25. package/frontend/components/preview/browser/BrowserPreview.svelte +15 -0
  26. package/frontend/components/preview/browser/components/Canvas.svelte +432 -69
  27. package/frontend/components/preview/browser/components/Container.svelte +23 -1
  28. package/frontend/components/preview/browser/components/Toolbar.svelte +1 -8
  29. package/frontend/components/preview/browser/core/coordinator.svelte.ts +27 -4
  30. package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +36 -3
  31. package/frontend/components/preview/browser/core/tab-operations.svelte.ts +1 -0
  32. package/frontend/components/terminal/TerminalTabs.svelte +1 -2
  33. package/frontend/components/workspace/PanelHeader.svelte +15 -0
  34. package/frontend/components/workspace/panels/FilesPanel.svelte +1 -0
  35. package/frontend/components/workspace/panels/GitPanel.svelte +27 -13
  36. package/frontend/components/workspace/panels/PreviewPanel.svelte +2 -0
  37. package/frontend/services/chat/chat.service.ts +9 -8
  38. package/frontend/services/preview/browser/browser-webcodecs.service.ts +43 -138
  39. package/frontend/stores/core/app.svelte.ts +4 -3
  40. package/frontend/stores/core/presence.svelte.ts +3 -2
  41. package/frontend/stores/core/sessions.svelte.ts +2 -0
  42. package/frontend/stores/ui/notification.svelte.ts +4 -1
  43. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
 
5
5
  interface Props {
6
6
  isLoading: boolean;
7
+ isCancelling: boolean;
7
8
  hasActiveProject: boolean;
8
9
  messageText: string;
9
10
  attachedFiles: FileAttachment[];
@@ -16,6 +17,7 @@
16
17
 
17
18
  const {
18
19
  isLoading,
20
+ isCancelling,
19
21
  hasActiveProject,
20
22
  messageText,
21
23
  attachedFiles,
@@ -65,6 +67,14 @@
65
67
  >
66
68
  <Icon name="lucide:circle-stop" class="text-white w-5 h-5" />
67
69
  </button>
70
+ {:else if isCancelling}
71
+ <button
72
+ disabled
73
+ class="w-10 h-10 bg-red-500 opacity-70 rounded-xl flex items-center justify-center transition-all duration-200 cursor-not-allowed"
74
+ aria-label="Cancelling..."
75
+ >
76
+ <Icon name="lucide:loader-circle" class="text-white w-5 h-5 animate-spin" />
77
+ </button>
68
78
  {:else}
69
79
  <button
70
80
  onclick={onSend}
@@ -10,7 +10,6 @@ import { debug } from '$shared/utils/logger';
10
10
  import type { FileAttachment } from './use-file-handling.svelte';
11
11
 
12
12
  interface ChatActionsParams {
13
- messageText: string;
14
13
  attachedFiles: FileAttachment[];
15
14
  clearAllAttachments: () => void;
16
15
  adjustTextareaHeight: () => void;
@@ -27,7 +26,6 @@ export function useChatActions(params: ChatActionsParams) {
27
26
  function handleCancelEdit() {
28
27
  cancelEdit();
29
28
  clearInput();
30
- params.messageText = ''; // This won't work directly, need to pass setter
31
29
  params.clearAllAttachments();
32
30
  params.adjustTextareaHeight();
33
31
  }
@@ -52,6 +50,12 @@ export function useChatActions(params: ChatActionsParams) {
52
50
  await snapshotService.restore(restoreTargetId, sessionState.currentSession.id);
53
51
  }
54
52
 
53
+ // Set skip and clear draft BEFORE reloading messages — editing the first message
54
+ // causes messages to become empty (welcome state), which remounts ChatInput.
55
+ // Without this, the new instance restores stale server state into the input.
56
+ setSkipNextRestore(true);
57
+ params.clearDraft();
58
+
55
59
  // Reload messages from database to update UI
56
60
  if (sessionState.currentSession?.id) {
57
61
  await loadMessagesForSession(sessionState.currentSession.id);
@@ -9,6 +9,7 @@
9
9
  -->
10
10
 
11
11
  <script lang="ts">
12
+ import { tick } from 'svelte';
12
13
  import type { SDKMessageFormatter } from '$shared/types/database/schema';
13
14
  import type { IconName } from '$shared/types/ui/icons';
14
15
  import Card from '$frontend/components/common/display/Card.svelte';
@@ -46,6 +47,23 @@
46
47
  onShowTokenUsage: () => void;
47
48
  onShowDebug: () => void;
48
49
  } = $props();
50
+
51
+ let scrollContainer: HTMLDivElement | undefined = $state();
52
+
53
+ // Auto-scroll reasoning/system content to bottom while receiving partial text
54
+ $effect(() => {
55
+ if (roleCategory !== 'reasoning' && roleCategory !== 'system') return;
56
+ if (!scrollContainer) return;
57
+ // Track message content changes (partialText for streaming, message for final)
58
+ const _track = message.type === 'stream_event' && 'partialText' in message
59
+ ? message.partialText
60
+ : message;
61
+ tick().then(() => {
62
+ if (scrollContainer) {
63
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
64
+ }
65
+ });
66
+ });
49
67
  </script>
50
68
 
51
69
  <div class="relative overflow-hidden">
@@ -73,7 +91,10 @@
73
91
  />
74
92
 
75
93
  <!-- Message Content -->
76
- <div class="p-3 md:p-4">
94
+ <div
95
+ bind:this={scrollContainer}
96
+ class="p-3 md:p-4 {roleCategory === 'reasoning' || roleCategory === 'system' ? 'max-h-80 overflow-y-auto' : ''}"
97
+ >
77
98
  <div class="max-w-none space-y-4">
78
99
  <!-- Content rendering using MessageFormatter component -->
79
100
  <MessageFormatter {message} />
@@ -33,14 +33,14 @@
33
33
  <span class="text-xs font-medium text-slate-700 dark:text-slate-300">Command:</span>
34
34
  </div>
35
35
  {#if timeout}
36
- <div class="inline-block ml-auto text-xs bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300 px-2 py-0.5 rounded">
36
+ <div class="inline-block ml-auto text-3xs bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300 px-2 py-0.5 rounded">
37
37
  Timeout: {timeout}ms
38
38
  </div>
39
39
  {/if}
40
40
  </div>
41
41
 
42
42
  <!-- Terminal-style command display -->
43
- <div class="bg-slate-50 dark:bg-slate-950 border border-slate-200/60 dark:border-slate-800/60 rounded-md p-2.5 font-mono text-sm">
43
+ <div class="max-h-64 overflow-y-auto bg-slate-50 dark:bg-slate-950 border border-slate-200/60 dark:border-slate-800/60 rounded-md p-2.5 font-mono text-sm">
44
44
  <div class="flex items-start gap-2">
45
45
  <span class="text-green-600 dark:text-green-400 select-none">$</span>
46
46
  <div class="flex-1 text-slate-900 dark:text-slate-200 break-all">
@@ -40,6 +40,7 @@
40
40
  externallyChanged?: boolean;
41
41
  onForceReload?: () => void;
42
42
  isBinary?: boolean;
43
+ projectPath?: string;
43
44
  }
44
45
 
45
46
  const {
@@ -56,9 +57,19 @@
56
57
  onToggleWordWrap,
57
58
  externallyChanged = false,
58
59
  onForceReload,
59
- isBinary = false
60
+ isBinary = false,
61
+ projectPath = ''
60
62
  }: Props = $props();
61
63
 
64
+ // Relative path for display
65
+ const displayPath = $derived.by(() => {
66
+ if (!file) return '';
67
+ if (projectPath && file.path.startsWith(projectPath)) {
68
+ return file.path.slice(projectPath.length).replace(/^[/\\]/, '');
69
+ }
70
+ return file.path;
71
+ });
72
+
62
73
  // Theme state
63
74
  const isDark = $derived(themeStore.isDark);
64
75
  const monacoTheme = $derived(isDark ? 'vs-dark' : 'vs-light');
@@ -273,7 +284,7 @@
273
284
  {file.name}
274
285
  </h3>
275
286
  <p class="text-xs text-slate-600 dark:text-slate-400 truncate mt-0.5">
276
- <span class="hidden sm:inline">{file.path} • </span> {formatFileSize(file.size || 0)}
287
+ <span class="hidden sm:inline">{displayPath} • </span> {formatFileSize(file.size || 0)}
277
288
  </p>
278
289
  </div>
279
290
  </div>
@@ -428,7 +428,7 @@
428
428
  <span
429
429
  class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {isSessionWaitingInput(session.id, projectState.currentProject?.id) ? 'bg-amber-500' : 'bg-emerald-500'}"
430
430
  ></span>
431
- {:else if isSessionUnread(session.id)}
431
+ {:else if isSessionUnread(session.id) && session.id !== sessionState.currentSession?.id}
432
432
  <span
433
433
  class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 bg-blue-500"
434
434
  ></span>
@@ -61,6 +61,9 @@
61
61
  // Flag to prevent URL watcher from double-launching during MCP session creation
62
62
  const mcpLaunchInProgress = $state(false);
63
63
 
64
+ // Touch interaction mode for canvas
65
+ let touchMode = $state<'scroll' | 'cursor'>('scroll');
66
+
64
67
  // Flag to track if sessions were recovered (prevents creating empty tab on mount)
65
68
  let sessionsRecovered = $state(false);
66
69
 
@@ -160,6 +163,7 @@
160
163
  if (currentProjectId && currentProjectId !== previousProjectId && previousProjectId !== '') {
161
164
  debug.log('preview', `🔄 Project changed: ${previousProjectId} → ${currentProjectId}`);
162
165
  previousProjectId = currentProjectId;
166
+ sessionsRecovered = false; // Reset for new project (enables empty tab creation if needed)
163
167
  coordinator.switchProject();
164
168
  }
165
169
  });
@@ -416,6 +420,14 @@
416
420
  return mcpHandler.isCurrentTabMcpControlled();
417
421
  }
418
422
 
423
+ // Hide MCP virtual cursor when switching to a non-MCP-controlled tab
424
+ $effect(() => {
425
+ void activeTabId; // track activeTabId changes
426
+ if (!isCurrentTabMcpControlled()) {
427
+ mcpVirtualCursor = { x: 0, y: 0, visible: false, clicking: false };
428
+ }
429
+ });
430
+
419
431
  // Stream message handling
420
432
  $effect(() => {
421
433
  if (activeTabId && sessionId) {
@@ -425,6 +437,8 @@
425
437
 
426
438
  // Expose methods for parent (PreviewPanel)
427
439
  export const browserActions = {
440
+ getTouchMode: () => touchMode,
441
+ setTouchMode: (mode: 'scroll' | 'cursor') => { touchMode = mode; },
428
442
  changeDeviceSize: (size: DeviceSize) => {
429
443
  coordinator.changeDeviceSize(size, previewDimensions?.scale);
430
444
  },
@@ -493,6 +507,7 @@
493
507
  bind:canvasAPI
494
508
  bind:previewDimensions
495
509
  bind:lastFrameData={currentTabLastFrameData}
510
+ bind:touchMode
496
511
  isMcpControlled={isCurrentTabMcpControlled()}
497
512
  onInteraction={handleCanvasInteraction}
498
513
  onRetry={handleGoClick}