@myrialabs/clopen 0.2.11 → 0.2.13
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 +106 -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 +14 -3
- package/backend/engine/types.ts +9 -0
- package/backend/index.ts +13 -2
- package/backend/mcp/config.ts +32 -6
- package/backend/snapshot/blob-store.ts +52 -72
- package/backend/snapshot/snapshot-service.ts +24 -0
- package/backend/terminal/stream-manager.ts +121 -131
- 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/backend/ws/terminal/persistence.ts +19 -33
- package/backend/ws/terminal/session.ts +37 -19
- package/bun.lock +6 -0
- package/frontend/components/auth/SetupPage.svelte +1 -1
- package/frontend/components/chat/input/ChatInput.svelte +22 -1
- package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
- package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
- package/frontend/components/chat/message/MessageBubble.svelte +13 -0
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
- 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/git/ChangesSection.svelte +104 -13
- 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/terminal/Terminal.svelte +5 -1
- 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 +94 -23
- package/frontend/services/notification/global-stream-monitor.ts +5 -2
- package/frontend/services/terminal/project.service.ts +4 -60
- package/frontend/services/terminal/terminal.service.ts +18 -27
- package/frontend/stores/core/app.svelte.ts +10 -2
- package/frontend/stores/core/sessions.svelte.ts +10 -1
- package/package.json +4 -2
|
@@ -43,16 +43,14 @@ class ChatService {
|
|
|
43
43
|
|
|
44
44
|
static loadingTexts: string[] = [
|
|
45
45
|
'thinking', 'processing', 'analyzing', 'calculating', 'computing',
|
|
46
|
-
'strategizing', 'learningpatterns', '
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'bootingreasoning', 'activatingmodules', 'triggeringaction', 'deployinglogic',
|
|
55
|
-
'maintainingstate', 'clearingcache', 'updating', 'reflecting', 'syncinglogic',
|
|
46
|
+
'strategizing', 'learningpatterns', 'adaptingmodels', 'evaluatingoptions',
|
|
47
|
+
'executingplans', 'simulatingscenarios', 'predictingoutcomes', 'planningactions',
|
|
48
|
+
'processinginputs', 'optimizing', 'generatingresponses', 'refininglogic',
|
|
49
|
+
'validatingoutputs', 'modulatingresponse', 'updatingmemory', 'recognizingpatterns',
|
|
50
|
+
'switchingcontext', 'allocatingresources', 'prioritizingtasks',
|
|
51
|
+
'developingawareness', 'buildingstrategies', 'assessingscenarios',
|
|
52
|
+
'bootingreasoning', 'triggeringaction', 'deployinglogic', 'synthesizinginformation',
|
|
53
|
+
'maintainingstate', 'updating', 'reflecting', 'syncinglogic',
|
|
56
54
|
'connectingdots', 'compilingideas', 'brainstorming', 'schedulingtasks'
|
|
57
55
|
].map(text => text + '...');
|
|
58
56
|
|
|
@@ -482,11 +480,11 @@ class ChatService {
|
|
|
482
480
|
// and global flags — cancel sets isCancelling=true to prevent presence re-enabling
|
|
483
481
|
this.setProcessState({ isLoading: false, isWaitingInput: false, isCancelling: true }, chatSessionId);
|
|
484
482
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
this.
|
|
483
|
+
// Convert stream_events to finalized assistant messages on cancel.
|
|
484
|
+
// This preserves partial reasoning/text that was visible to the user.
|
|
485
|
+
// Empty stream_events are removed. The backend saves partial text to DB
|
|
486
|
+
// independently, so on refresh the DB version takes over.
|
|
487
|
+
this.finalizeStreamEvents();
|
|
490
488
|
|
|
491
489
|
// Safety timeout: if backend events (chat:cancelled + presence update) don't
|
|
492
490
|
// arrive within 10 seconds, force-clear isCancelling to prevent infinite loader.
|
|
@@ -581,15 +579,32 @@ class ChatService {
|
|
|
581
579
|
}
|
|
582
580
|
// If no reasoning stream_event found, fall through to push at end
|
|
583
581
|
} else {
|
|
584
|
-
//
|
|
585
|
-
//
|
|
582
|
+
// Replace text stream_event IN PLACE to preserve message position
|
|
583
|
+
// (same approach as reasoning — prevents visual displacement)
|
|
586
584
|
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
587
585
|
const msg = sessionState.messages[i] as any;
|
|
588
586
|
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
589
|
-
|
|
590
|
-
|
|
587
|
+
const messageFormatter = {
|
|
588
|
+
...sdkMessage,
|
|
589
|
+
metadata: buildMetadataFromTransport(data)
|
|
590
|
+
};
|
|
591
|
+
sessionState.messages[i] = messageFormatter;
|
|
592
|
+
|
|
593
|
+
// Detect interactive tool_use blocks in the replaced message
|
|
594
|
+
if (sdkMessage.type === 'assistant' && sdkMessage.message?.content) {
|
|
595
|
+
const content = Array.isArray(sdkMessage.message.content) ? sdkMessage.message.content : [];
|
|
596
|
+
const hasInteractiveTool = content.some(
|
|
597
|
+
(item: any) => item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name)
|
|
598
|
+
);
|
|
599
|
+
if (hasInteractiveTool) {
|
|
600
|
+
this.setProcessState({ isWaitingInput: true });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return; // Replaced in-place, skip push below
|
|
591
605
|
}
|
|
592
606
|
}
|
|
607
|
+
// No stream_event found, fall through to push at end
|
|
593
608
|
}
|
|
594
609
|
}
|
|
595
610
|
|
|
@@ -723,7 +738,7 @@ class ChatService {
|
|
|
723
738
|
const msg = sessionState.messages[i] as any;
|
|
724
739
|
if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
|
|
725
740
|
msg.partialText = partialText || '';
|
|
726
|
-
|
|
741
|
+
return;
|
|
727
742
|
}
|
|
728
743
|
}
|
|
729
744
|
} else {
|
|
@@ -733,17 +748,42 @@ class ChatService {
|
|
|
733
748
|
const msg = sessionState.messages[i] as any;
|
|
734
749
|
if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
|
|
735
750
|
msg.partialText = partialText || '';
|
|
736
|
-
|
|
751
|
+
return;
|
|
737
752
|
}
|
|
738
753
|
}
|
|
739
754
|
}
|
|
755
|
+
|
|
756
|
+
// Fallback: no matching stream_event found (start event was missed).
|
|
757
|
+
// Create one now so text doesn't get lost.
|
|
758
|
+
const fallbackMessage = {
|
|
759
|
+
type: 'stream_event' as const,
|
|
760
|
+
processId: data.processId,
|
|
761
|
+
partialText: partialText || '',
|
|
762
|
+
metadata: buildMetadataFromTransport({
|
|
763
|
+
timestamp: data.timestamp,
|
|
764
|
+
...(isReasoning && { reasoning: true }),
|
|
765
|
+
})
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
if (isReasoning) {
|
|
769
|
+
const textStreamIdx = (sessionState.messages as any[]).findIndex(
|
|
770
|
+
(m: any) => m.type === 'stream_event' && !m.metadata?.reasoning
|
|
771
|
+
);
|
|
772
|
+
if (textStreamIdx >= 0) {
|
|
773
|
+
(sessionState.messages as any[]).splice(textStreamIdx, 0, fallbackMessage);
|
|
774
|
+
} else {
|
|
775
|
+
(sessionState.messages as any[]).push(fallbackMessage);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
(sessionState.messages as any[]).push(fallbackMessage);
|
|
779
|
+
}
|
|
740
780
|
}
|
|
741
781
|
// Note: 'end' event is not needed - streaming message will be replaced by final message in handleMessageEvent
|
|
742
782
|
}
|
|
743
783
|
|
|
744
784
|
/**
|
|
745
785
|
* Remove all stream_event messages from the messages array.
|
|
746
|
-
* Called on
|
|
786
|
+
* Called on new message send to prevent stale streaming
|
|
747
787
|
* placeholders from causing wrong insertion positions.
|
|
748
788
|
*/
|
|
749
789
|
private cleanupStreamEvents(): void {
|
|
@@ -754,6 +794,36 @@ class ChatService {
|
|
|
754
794
|
}
|
|
755
795
|
}
|
|
756
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Convert stream_event messages with text to finalized assistant messages.
|
|
799
|
+
* Called on cancel to preserve partial reasoning/text that was visible.
|
|
800
|
+
* Empty stream_events (no text) are removed.
|
|
801
|
+
* The backend saves these to DB independently, so on refresh the DB version takes over.
|
|
802
|
+
*/
|
|
803
|
+
private finalizeStreamEvents(): void {
|
|
804
|
+
for (let i = sessionState.messages.length - 1; i >= 0; i--) {
|
|
805
|
+
const msg = sessionState.messages[i] as any;
|
|
806
|
+
if (msg.type !== 'stream_event') continue;
|
|
807
|
+
|
|
808
|
+
if (msg.partialText) {
|
|
809
|
+
const isReasoning = msg.metadata?.reasoning === true;
|
|
810
|
+
sessionState.messages[i] = {
|
|
811
|
+
type: 'assistant',
|
|
812
|
+
message: {
|
|
813
|
+
role: 'assistant',
|
|
814
|
+
content: [{ type: 'text', text: msg.partialText }]
|
|
815
|
+
},
|
|
816
|
+
metadata: {
|
|
817
|
+
...msg.metadata,
|
|
818
|
+
...(isReasoning && { reasoning: true }),
|
|
819
|
+
}
|
|
820
|
+
} as any;
|
|
821
|
+
} else {
|
|
822
|
+
sessionState.messages.splice(i, 1);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
757
827
|
/**
|
|
758
828
|
* Detect whether any interactive tool (e.g. AskUserQuestion) is pending in the current messages.
|
|
759
829
|
* Used after browser refresh / catchup to restore the isWaitingInput state.
|
|
@@ -774,10 +844,11 @@ class ChatService {
|
|
|
774
844
|
}
|
|
775
845
|
}
|
|
776
846
|
|
|
777
|
-
// Check if any interactive tool is unanswered
|
|
847
|
+
// Check if any interactive tool is unanswered (skip interrupted/cancelled messages)
|
|
778
848
|
for (const msg of sessionState.messages) {
|
|
779
849
|
const msgAny = msg as any;
|
|
780
850
|
if (msgAny.type !== 'assistant' || !msgAny.message?.content) continue;
|
|
851
|
+
if (msgAny.metadata?.interrupted) continue;
|
|
781
852
|
const content = Array.isArray(msgAny.message.content) ? msgAny.message.content : [];
|
|
782
853
|
const hasPendingInteractive = content.some(
|
|
783
854
|
(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();
|
|
@@ -262,66 +262,10 @@ class TerminalProjectManager {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// First, restore base output from context (input/output sebelumnya)
|
|
270
|
-
if (context.sessionOutputs.has(sessionId)) {
|
|
271
|
-
const savedOutput = context.sessionOutputs.get(sessionId);
|
|
272
|
-
if (savedOutput) {
|
|
273
|
-
baseOutput = savedOutput.map(output => ({
|
|
274
|
-
content: output.content,
|
|
275
|
-
type: output.type as any,
|
|
276
|
-
timestamp: output.timestamp
|
|
277
|
-
}));
|
|
278
|
-
// Restored base output lines for session
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Second, get NEW output from server that was generated while we were away
|
|
283
|
-
// We need to track when we saved the output to know what's new
|
|
284
|
-
if (hasActiveStream && activeStreamInfo) {
|
|
285
|
-
try {
|
|
286
|
-
// Get the saved output count from context metadata
|
|
287
|
-
// This tells us how much output we had when we switched away
|
|
288
|
-
let savedOutputCount = 0;
|
|
289
|
-
const savedMetadata = context.sessionOutputs.get(`${sessionId}-metadata`);
|
|
290
|
-
if (savedMetadata && typeof savedMetadata === 'object' && 'outputCount' in savedMetadata) {
|
|
291
|
-
savedOutputCount = (savedMetadata as any).outputCount || 0;
|
|
292
|
-
} else {
|
|
293
|
-
// Fallback: count actual output lines in baseOutput
|
|
294
|
-
for (const line of baseOutput) {
|
|
295
|
-
if (line.type === 'output' || line.type === 'error') {
|
|
296
|
-
savedOutputCount++;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Get only NEW output from server (skip what we already have)
|
|
302
|
-
const data = await terminalService.getMissedOutput(
|
|
303
|
-
sessionId,
|
|
304
|
-
activeStreamInfo.streamId,
|
|
305
|
-
savedOutputCount
|
|
306
|
-
);
|
|
307
|
-
if (data.success && data.output && data.output.length > 0) {
|
|
308
|
-
// Convert server output to terminal lines
|
|
309
|
-
backgroundOutput = data.output.map((content: string) => ({
|
|
310
|
-
content: content,
|
|
311
|
-
type: 'output',
|
|
312
|
-
timestamp: new Date()
|
|
313
|
-
}));
|
|
314
|
-
debug.log('terminal', `Restored ${backgroundOutput.length} new output lines for session ${sessionId}`);
|
|
315
|
-
}
|
|
316
|
-
} catch (error) {
|
|
317
|
-
debug.error('terminal', 'Failed to fetch missed output:', error);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Combine base output with background output from server
|
|
322
|
-
// Base output contains previous input/output saved in context
|
|
323
|
-
// Background output contains new output from server (if any)
|
|
324
|
-
terminalSession.lines = [...baseOutput, ...backgroundOutput];
|
|
265
|
+
// Always start with empty lines.
|
|
266
|
+
// The create-session replay from headless xterm will provide the
|
|
267
|
+
// accurate current terminal state (respects clear, scrollback, etc.)
|
|
268
|
+
terminalSession.lines = [];
|
|
325
269
|
|
|
326
270
|
// Restore command history from context (persisted) rather than manager (temporary)
|
|
327
271
|
const savedCommandHistory = context.sessionCommandHistories.get(sessionId);
|
|
@@ -62,20 +62,6 @@ export class TerminalService {
|
|
|
62
62
|
// Create unique stream ID for this connection
|
|
63
63
|
const streamId = `${sessionId}-${Date.now()}`;
|
|
64
64
|
|
|
65
|
-
// Get current output count to mark where new output starts
|
|
66
|
-
let outputStartIndex = 0;
|
|
67
|
-
if (typeof window !== 'undefined') {
|
|
68
|
-
try {
|
|
69
|
-
const terminalStoreModule = await import('$frontend/stores/features/terminal.svelte');
|
|
70
|
-
const termSession = terminalStoreModule.terminalStore.getSession(sessionId);
|
|
71
|
-
if (termSession && termSession.lines) {
|
|
72
|
-
outputStartIndex = termSession.lines.length;
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
// Ignore error, use default 0
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
65
|
// Setup WebSocket listeners for this session
|
|
80
66
|
const listeners: Array<() => void> = [];
|
|
81
67
|
|
|
@@ -173,8 +159,7 @@ export class TerminalService {
|
|
|
173
159
|
workingDirectory: session.workingDirectory,
|
|
174
160
|
projectPath,
|
|
175
161
|
cols: terminalSize?.cols || 80,
|
|
176
|
-
rows: terminalSize?.rows || 24
|
|
177
|
-
outputStartIndex
|
|
162
|
+
rows: terminalSize?.rows || 24
|
|
178
163
|
});
|
|
179
164
|
|
|
180
165
|
debug.log('terminal', `✅ Terminal session created:`, response);
|
|
@@ -230,6 +215,17 @@ export class TerminalService {
|
|
|
230
215
|
}
|
|
231
216
|
}
|
|
232
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Clear headless terminal on backend (sync with frontend clear)
|
|
220
|
+
*/
|
|
221
|
+
async clearHeadlessTerminal(sessionId: string): Promise<void> {
|
|
222
|
+
try {
|
|
223
|
+
await ws.http('terminal:clear', { sessionId });
|
|
224
|
+
} catch {
|
|
225
|
+
// Silently handle - non-critical
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
233
229
|
/**
|
|
234
230
|
* Resize terminal for a specific session
|
|
235
231
|
*/
|
|
@@ -301,39 +297,34 @@ export class TerminalService {
|
|
|
301
297
|
}
|
|
302
298
|
|
|
303
299
|
/**
|
|
304
|
-
* Get missed output for a session
|
|
300
|
+
* Get missed output for a session (serialized terminal state)
|
|
305
301
|
*/
|
|
306
302
|
async getMissedOutput(
|
|
307
303
|
sessionId: string,
|
|
308
|
-
streamId?: string
|
|
309
|
-
fromIndex: number = 0
|
|
304
|
+
streamId?: string
|
|
310
305
|
): Promise<{
|
|
311
306
|
success: boolean;
|
|
312
|
-
output: string
|
|
313
|
-
outputCount: number;
|
|
307
|
+
output: string;
|
|
314
308
|
status: string;
|
|
315
309
|
}> {
|
|
316
310
|
try {
|
|
317
|
-
const data = await ws.http('terminal:missed-output', { sessionId, streamId
|
|
311
|
+
const data = await ws.http('terminal:missed-output', { sessionId, streamId }, 5000);
|
|
318
312
|
if (data.sessionId === sessionId) {
|
|
319
313
|
return {
|
|
320
314
|
success: true,
|
|
321
315
|
output: data.output,
|
|
322
|
-
outputCount: data.outputCount,
|
|
323
316
|
status: data.status
|
|
324
317
|
};
|
|
325
318
|
}
|
|
326
319
|
return {
|
|
327
320
|
success: false,
|
|
328
|
-
output:
|
|
329
|
-
outputCount: 0,
|
|
321
|
+
output: '',
|
|
330
322
|
status: 'invalid_session'
|
|
331
323
|
};
|
|
332
324
|
} catch {
|
|
333
325
|
return {
|
|
334
326
|
success: false,
|
|
335
|
-
output:
|
|
336
|
-
outputCount: 0,
|
|
327
|
+
output: '',
|
|
337
328
|
status: 'timeout'
|
|
338
329
|
};
|
|
339
330
|
}
|
|
@@ -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;
|
|
@@ -433,6 +436,12 @@ export async function initializeSessions() {
|
|
|
433
436
|
setupCollaborativeListeners();
|
|
434
437
|
setupEditModeListener();
|
|
435
438
|
|
|
439
|
+
// Skip loading if no project is active — both calls require WS project context
|
|
440
|
+
if (!projectState.currentProject) {
|
|
441
|
+
debug.log('session', 'No active project, skipping session load');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
436
445
|
// Load sessions and restore edit mode in parallel
|
|
437
446
|
// Both only need WS project context (already set by initializeProjects)
|
|
438
447
|
await Promise.all([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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",
|
|
@@ -77,17 +77,19 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@anthropic-ai/claude-agent-sdk": "0.2.63",
|
|
79
79
|
"@anthropic-ai/sdk": "0.78.0",
|
|
80
|
-
"@opencode-ai/sdk": "1.2.15",
|
|
81
80
|
"@elysiajs/cors": "^1.4.0",
|
|
82
81
|
"@iconify-json/lucide": "^1.2.57",
|
|
83
82
|
"@iconify-json/material-icon-theme": "^1.2.16",
|
|
84
83
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
85
84
|
"@monaco-editor/loader": "^1.5.0",
|
|
85
|
+
"@opencode-ai/sdk": "1.2.15",
|
|
86
86
|
"@xterm/addon-clipboard": "^0.2.0",
|
|
87
87
|
"@xterm/addon-fit": "^0.11.0",
|
|
88
88
|
"@xterm/addon-ligatures": "^0.10.0",
|
|
89
|
+
"@xterm/addon-serialize": "^0.14.0",
|
|
89
90
|
"@xterm/addon-unicode11": "^0.9.0",
|
|
90
91
|
"@xterm/addon-web-links": "^0.12.0",
|
|
92
|
+
"@xterm/headless": "^6.0.0",
|
|
91
93
|
"@xterm/xterm": "^6.0.0",
|
|
92
94
|
"bun-pty": "^0.4.2",
|
|
93
95
|
"cloudflared": "^0.7.1",
|