@myrialabs/clopen 0.2.11 → 0.2.12
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 +103 -9
- package/backend/database/queries/project-queries.ts +1 -4
- package/backend/database/queries/session-queries.ts +36 -1
- package/backend/database/queries/snapshot-queries.ts +122 -0
- package/backend/database/utils/connection.ts +17 -11
- package/backend/engine/adapters/claude/stream.ts +12 -2
- package/backend/index.ts +13 -2
- package/backend/snapshot/blob-store.ts +52 -72
- package/backend/snapshot/snapshot-service.ts +24 -0
- package/backend/terminal/stream-manager.ts +41 -2
- package/backend/ws/chat/stream.ts +14 -7
- package/backend/ws/engine/claude/accounts.ts +6 -8
- package/backend/ws/projects/crud.ts +72 -7
- package/backend/ws/sessions/crud.ts +119 -2
- package/backend/ws/system/operations.ts +14 -39
- package/frontend/components/auth/SetupPage.svelte +1 -1
- package/frontend/components/chat/input/ChatInput.svelte +14 -1
- package/frontend/components/chat/message/MessageBubble.svelte +13 -0
- package/frontend/components/common/form/FolderBrowser.svelte +17 -4
- package/frontend/components/common/overlay/Dialog.svelte +17 -15
- package/frontend/components/files/FileNode.svelte +0 -15
- package/frontend/components/history/HistoryModal.svelte +94 -19
- package/frontend/components/history/HistoryView.svelte +29 -36
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/components/settings/general/DataManagementSettings.svelte +1 -54
- package/frontend/components/workspace/DesktopNavigator.svelte +57 -10
- package/frontend/components/workspace/MobileNavigator.svelte +57 -10
- package/frontend/components/workspace/WorkspaceLayout.svelte +0 -8
- package/frontend/services/chat/chat.service.ts +86 -13
- package/frontend/services/notification/global-stream-monitor.ts +5 -2
- package/frontend/stores/core/app.svelte.ts +10 -2
- package/frontend/stores/core/sessions.svelte.ts +4 -1
- package/package.json +1 -1
|
@@ -482,11 +482,11 @@ class ChatService {
|
|
|
482
482
|
// and global flags — cancel sets isCancelling=true to prevent presence re-enabling
|
|
483
483
|
this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: true }, chatSessionId);
|
|
484
484
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
this.
|
|
485
|
+
// Convert stream_events to finalized assistant messages on cancel.
|
|
486
|
+
// This preserves partial reasoning/text that was visible to the user.
|
|
487
|
+
// Empty stream_events are removed. The backend saves partial text to DB
|
|
488
|
+
// independently, so on refresh the DB version takes over.
|
|
489
|
+
this.finalizeStreamEvents();
|
|
490
490
|
|
|
491
491
|
// Safety timeout: if backend events (chat:cancelled + presence update) don't
|
|
492
492
|
// arrive within 10 seconds, force-clear isCancelling to prevent infinite loader.
|
|
@@ -581,15 +581,32 @@ class ChatService {
|
|
|
581
581
|
}
|
|
582
582
|
// If no reasoning stream_event found, fall through to push at end
|
|
583
583
|
} else {
|
|
584
|
-
//
|
|
585
|
-
//
|
|
584
|
+
// Replace text stream_event IN PLACE to preserve message position
|
|
585
|
+
// (same approach as reasoning — prevents visual displacement)
|
|
586
586
|
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
587
587
|
const msg = sessionState.messages[i] as any;
|
|
588
588
|
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
const messageFormatter = {
|
|
590
|
+
...sdkMessage,
|
|
591
|
+
metadata: buildMetadataFromTransport(data)
|
|
592
|
+
};
|
|
593
|
+
sessionState.messages[i] = messageFormatter;
|
|
594
|
+
|
|
595
|
+
// Detect interactive tool_use blocks in the replaced message
|
|
596
|
+
if (sdkMessage.type === 'assistant' && sdkMessage.message?.content) {
|
|
597
|
+
const content = Array.isArray(sdkMessage.message.content) ? sdkMessage.message.content : [];
|
|
598
|
+
const hasInteractiveTool = content.some(
|
|
599
|
+
(item: any) => item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name)
|
|
600
|
+
);
|
|
601
|
+
if (hasInteractiveTool) {
|
|
602
|
+
this.setProcessState({ isWaitingInput: true });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return; // Replaced in-place, skip push below
|
|
591
607
|
}
|
|
592
608
|
}
|
|
609
|
+
// No stream_event found, fall through to push at end
|
|
593
610
|
}
|
|
594
611
|
}
|
|
595
612
|
|
|
@@ -723,7 +740,7 @@ class ChatService {
|
|
|
723
740
|
const msg = sessionState.messages[i] as any;
|
|
724
741
|
if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
|
|
725
742
|
msg.partialText = partialText || '';
|
|
726
|
-
|
|
743
|
+
return;
|
|
727
744
|
}
|
|
728
745
|
}
|
|
729
746
|
} else {
|
|
@@ -733,17 +750,42 @@ class ChatService {
|
|
|
733
750
|
const msg = sessionState.messages[i] as any;
|
|
734
751
|
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
735
752
|
msg.partialText = partialText || '';
|
|
736
|
-
|
|
753
|
+
return;
|
|
737
754
|
}
|
|
738
755
|
}
|
|
739
756
|
}
|
|
757
|
+
|
|
758
|
+
// Fallback: no matching stream_event found (start event was missed).
|
|
759
|
+
// Create one now so text doesn't get lost.
|
|
760
|
+
const fallbackMessage = {
|
|
761
|
+
type: 'stream_event' as const,
|
|
762
|
+
processId: data.processId,
|
|
763
|
+
partialText: partialText || '',
|
|
764
|
+
metadata: buildMetadataFromTransport({
|
|
765
|
+
timestamp: data.timestamp,
|
|
766
|
+
...(isReasoning && { reasoning: true }),
|
|
767
|
+
})
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
if (isReasoning) {
|
|
771
|
+
const textStreamIdx = (sessionState.messages as any[]).findIndex(
|
|
772
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning
|
|
773
|
+
);
|
|
774
|
+
if (textStreamIdx >= 0) {
|
|
775
|
+
(sessionState.messages as any[]).splice(textStreamIdx, 0, fallbackMessage);
|
|
776
|
+
} else {
|
|
777
|
+
(sessionState.messages as any[]).push(fallbackMessage);
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
(sessionState.messages as any[]).push(fallbackMessage);
|
|
781
|
+
}
|
|
740
782
|
}
|
|
741
783
|
// Note: 'end' event is not needed - streaming message will be replaced by final message in handleMessageEvent
|
|
742
784
|
}
|
|
743
785
|
|
|
744
786
|
/**
|
|
745
787
|
* Remove all stream_event messages from the messages array.
|
|
746
|
-
* Called on
|
|
788
|
+
* Called on new message send to prevent stale streaming
|
|
747
789
|
* placeholders from causing wrong insertion positions.
|
|
748
790
|
*/
|
|
749
791
|
private cleanupStreamEvents(): void {
|
|
@@ -754,6 +796,36 @@ class ChatService {
|
|
|
754
796
|
}
|
|
755
797
|
}
|
|
756
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Convert stream_event messages with text to finalized assistant messages.
|
|
801
|
+
* Called on cancel to preserve partial reasoning/text that was visible.
|
|
802
|
+
* Empty stream_events (no text) are removed.
|
|
803
|
+
* The backend saves these to DB independently, so on refresh the DB version takes over.
|
|
804
|
+
*/
|
|
805
|
+
private finalizeStreamEvents(): void {
|
|
806
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
807
|
+
const msg = sessionState.messages[i] as any;
|
|
808
|
+
if (msg.type !== 'stream_event') continue;
|
|
809
|
+
|
|
810
|
+
if (msg.partialText) {
|
|
811
|
+
const isReasoning = msg.metadata?.reasoning === true;
|
|
812
|
+
sessionState.messages[i] = {
|
|
813
|
+
type: 'assistant',
|
|
814
|
+
message: {
|
|
815
|
+
role: 'assistant',
|
|
816
|
+
content: [{ type: 'text', text: msg.partialText }]
|
|
817
|
+
},
|
|
818
|
+
metadata: {
|
|
819
|
+
...msg.metadata,
|
|
820
|
+
...(isReasoning && { reasoning: true }),
|
|
821
|
+
}
|
|
822
|
+
} as any;
|
|
823
|
+
} else {
|
|
824
|
+
sessionState.messages.splice(i, 1);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
757
829
|
/**
|
|
758
830
|
* Detect whether any interactive tool (e.g. AskUserQuestion) is pending in the current messages.
|
|
759
831
|
* Used after browser refresh / catchup to restore the isWaitingInput state.
|
|
@@ -774,10 +846,11 @@ class ChatService {
|
|
|
774
846
|
}
|
|
775
847
|
}
|
|
776
848
|
|
|
777
|
-
// Check if any interactive tool is unanswered
|
|
849
|
+
// Check if any interactive tool is unanswered (skip interrupted/cancelled messages)
|
|
778
850
|
for (const msg of sessionState.messages) {
|
|
779
851
|
const msgAny = msg as any;
|
|
780
852
|
if (msgAny.type !== 'assistant' || !msgAny.message?.content) continue;
|
|
853
|
+
if (msgAny.metadata?.interrupted) continue;
|
|
781
854
|
const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
|
|
782
855
|
const hasPendingInteractive = content.some(
|
|
783
856
|
(item: any) => item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name) && item.id && !answeredToolIds.has(item.id)
|
|
@@ -35,13 +35,16 @@ class GlobalStreamMonitor {
|
|
|
35
35
|
|
|
36
36
|
// Stream finished — notify on completion
|
|
37
37
|
ws.on('chat:stream-finished', async (data) => {
|
|
38
|
-
const { projectId, status, chatSessionId } = data;
|
|
38
|
+
const { projectId, status, chatSessionId, reason } = data;
|
|
39
39
|
|
|
40
|
-
debug.log('notification', 'GlobalStreamMonitor: Stream finished', { projectId, status });
|
|
40
|
+
debug.log('notification', 'GlobalStreamMonitor: Stream finished', { projectId, status, reason });
|
|
41
41
|
|
|
42
42
|
// Clean up notified IDs for this session (stream is done)
|
|
43
43
|
this.clearSessionNotifications(chatSessionId);
|
|
44
44
|
|
|
45
|
+
// Skip notifications when stream was cancelled due to session deletion
|
|
46
|
+
if (reason === 'session-deleted') return;
|
|
47
|
+
|
|
45
48
|
// Play sound notification
|
|
46
49
|
try {
|
|
47
50
|
await soundNotification.play();
|
|
@@ -129,10 +129,18 @@ export function syncGlobalStateFromSession(sessionId: string): void {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Remove
|
|
132
|
+
* Remove all app-level state for a deleted session
|
|
133
|
+
* (process state, unread status, etc.)
|
|
133
134
|
*/
|
|
134
|
-
export function
|
|
135
|
+
export function clearSessionState(sessionId: string): void {
|
|
135
136
|
delete appState.sessionStates[sessionId];
|
|
137
|
+
|
|
138
|
+
if (appState.unreadSessions.has(sessionId)) {
|
|
139
|
+
const next = new Map(appState.unreadSessions);
|
|
140
|
+
next.delete(sessionId);
|
|
141
|
+
appState.unreadSessions = next;
|
|
142
|
+
persistUnreadSessions();
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
// ========================================
|
|
@@ -13,7 +13,7 @@ import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
|
|
|
13
13
|
import ws, { onWsReconnect } from '$frontend/utils/ws';
|
|
14
14
|
import { projectState } from './projects.svelte';
|
|
15
15
|
import { setupEditModeListener, restoreEditMode } from '$frontend/stores/ui/edit-mode.svelte';
|
|
16
|
-
import { markSessionUnread, markSessionRead, appState } from '$frontend/stores/core/app.svelte';
|
|
16
|
+
import { markSessionUnread, markSessionRead, clearSessionState, appState } from '$frontend/stores/core/app.svelte';
|
|
17
17
|
import { debug } from '$shared/utils/logger';
|
|
18
18
|
|
|
19
19
|
interface SessionState {
|
|
@@ -164,6 +164,9 @@ export function removeSession(sessionId: string) {
|
|
|
164
164
|
sessionState.sessions.splice(index, 1);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// Clear all app-level state for this session (unread, process state)
|
|
168
|
+
clearSessionState(sessionId);
|
|
169
|
+
|
|
167
170
|
// Clear current session if it's the one being removed
|
|
168
171
|
if (sessionState.currentSession?.id === sessionId) {
|
|
169
172
|
sessionState.currentSession = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
|
|
5
5
|
"author": "Myria Labs",
|
|
6
6
|
"license": "MIT",
|