@myrialabs/clopen 0.2.12 → 0.2.14
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 +3 -0
- package/backend/engine/adapters/claude/stream.ts +2 -1
- package/backend/engine/types.ts +9 -0
- package/backend/mcp/config.ts +32 -6
- package/backend/snapshot/snapshot-service.ts +9 -7
- package/backend/terminal/stream-manager.ts +106 -155
- package/backend/ws/projects/crud.ts +3 -3
- package/backend/ws/snapshot/timeline.ts +6 -2
- package/backend/ws/terminal/persistence.ts +19 -33
- package/backend/ws/terminal/session.ts +37 -19
- package/bin/clopen.ts +376 -99
- package/bun.lock +6 -0
- package/frontend/components/chat/input/ChatInput.svelte +8 -0
- package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
- 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/widgets/FloatingTodoList.svelte +2 -2
- package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
- package/frontend/components/common/overlay/Dialog.svelte +2 -2
- package/frontend/components/git/ChangesSection.svelte +104 -13
- package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
- package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
- package/frontend/components/terminal/Terminal.svelte +5 -1
- package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
- package/frontend/services/chat/chat.service.ts +52 -11
- package/frontend/services/terminal/project.service.ts +4 -60
- package/frontend/services/terminal/terminal.service.ts +18 -27
- package/frontend/stores/core/sessions.svelte.ts +6 -0
- package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
- package/frontend/stores/ui/theme.svelte.ts +11 -11
- package/frontend/stores/ui/workspace.svelte.ts +1 -1
- package/index.html +2 -2
- package/package.json +4 -2
- package/shared/utils/anonymous-user.ts +4 -4
|
@@ -33,49 +33,38 @@ export const persistenceHandler = createRouter()
|
|
|
33
33
|
return status;
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
// Get missed output
|
|
36
|
+
// Get missed output (serialized terminal state)
|
|
37
37
|
.http('terminal:missed-output', {
|
|
38
38
|
data: t.Object({
|
|
39
39
|
sessionId: t.String(),
|
|
40
|
-
streamId: t.Optional(t.String())
|
|
41
|
-
fromIndex: t.Optional(t.Number())
|
|
40
|
+
streamId: t.Optional(t.String())
|
|
42
41
|
}),
|
|
43
42
|
response: t.Object({
|
|
44
43
|
sessionId: t.String(),
|
|
45
44
|
streamId: t.Union([t.String(), t.Null()]),
|
|
46
|
-
output: t.
|
|
47
|
-
outputCount: t.Number(),
|
|
45
|
+
output: t.String(),
|
|
48
46
|
status: t.String(),
|
|
49
|
-
fromIndex: t.Number(),
|
|
50
47
|
timestamp: t.String()
|
|
51
48
|
})
|
|
52
49
|
}, async ({ data }) => {
|
|
53
|
-
const { sessionId, streamId
|
|
50
|
+
const { sessionId, streamId } = data;
|
|
54
51
|
|
|
55
|
-
//
|
|
56
|
-
let output
|
|
52
|
+
// Get serialized terminal state from headless xterm
|
|
53
|
+
let output = '';
|
|
57
54
|
|
|
58
55
|
if (streamId) {
|
|
59
|
-
|
|
60
|
-
output = terminalStreamManager.getOutput(streamId, fromIndex);
|
|
56
|
+
output = terminalStreamManager.getSerializedOutput(streamId);
|
|
61
57
|
} else {
|
|
62
|
-
|
|
63
|
-
const cachedOutput = terminalStreamManager.loadCachedOutput(sessionId);
|
|
64
|
-
if (cachedOutput) {
|
|
65
|
-
output = cachedOutput.slice(fromIndex);
|
|
66
|
-
}
|
|
58
|
+
output = terminalStreamManager.getSerializedOutputBySession(sessionId);
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
// Get stream status if available
|
|
70
61
|
const streamStatus = streamId ? terminalStreamManager.getStreamStatus(streamId) : null;
|
|
71
62
|
|
|
72
63
|
return {
|
|
73
64
|
sessionId,
|
|
74
65
|
streamId: streamId || null,
|
|
75
66
|
output,
|
|
76
|
-
outputCount: output.length,
|
|
77
67
|
status: streamStatus?.status || 'unknown',
|
|
78
|
-
fromIndex,
|
|
79
68
|
timestamp: new Date().toISOString()
|
|
80
69
|
};
|
|
81
70
|
})
|
|
@@ -84,11 +73,10 @@ export const persistenceHandler = createRouter()
|
|
|
84
73
|
.on('terminal:reconnect', {
|
|
85
74
|
data: t.Object({
|
|
86
75
|
streamId: t.String(),
|
|
87
|
-
sessionId: t.String()
|
|
88
|
-
fromIndex: t.Optional(t.Number())
|
|
76
|
+
sessionId: t.String()
|
|
89
77
|
})
|
|
90
78
|
}, async ({ data, conn }) => {
|
|
91
|
-
const { streamId, sessionId
|
|
79
|
+
const { streamId, sessionId } = data;
|
|
92
80
|
const projectId = ws.getProjectId(conn);
|
|
93
81
|
|
|
94
82
|
const stream = terminalStreamManager.getStream(streamId);
|
|
@@ -102,17 +90,15 @@ export const persistenceHandler = createRouter()
|
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
try {
|
|
105
|
-
//
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
}
|
|
93
|
+
// Send serialized terminal state (frontend writes it to xterm to restore)
|
|
94
|
+
const serializedOutput = terminalStreamManager.getSerializedOutput(streamId);
|
|
95
|
+
|
|
96
|
+
if (serializedOutput) {
|
|
97
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
98
|
+
sessionId,
|
|
99
|
+
content: serializedOutput,
|
|
100
|
+
timestamp: new Date().toISOString()
|
|
101
|
+
});
|
|
116
102
|
}
|
|
117
103
|
|
|
118
104
|
if (stream.status === 'active') {
|
|
@@ -30,8 +30,7 @@ export const sessionHandler = createRouter()
|
|
|
30
30
|
workingDirectory: t.Optional(t.String()),
|
|
31
31
|
projectPath: t.Optional(t.String()),
|
|
32
32
|
cols: t.Optional(t.Number()),
|
|
33
|
-
rows: t.Optional(t.Number())
|
|
34
|
-
outputStartIndex: t.Optional(t.Number())
|
|
33
|
+
rows: t.Optional(t.Number())
|
|
35
34
|
}),
|
|
36
35
|
response: t.Object({
|
|
37
36
|
sessionId: t.String(),
|
|
@@ -48,8 +47,7 @@ export const sessionHandler = createRouter()
|
|
|
48
47
|
workingDirectory,
|
|
49
48
|
projectPath,
|
|
50
49
|
cols = 80,
|
|
51
|
-
rows = 24
|
|
52
|
-
outputStartIndex = 0
|
|
50
|
+
rows = 24
|
|
53
51
|
} = data;
|
|
54
52
|
|
|
55
53
|
const projectId = ws.getProjectId(conn);
|
|
@@ -120,7 +118,7 @@ export const sessionHandler = createRouter()
|
|
|
120
118
|
projectPath || '',
|
|
121
119
|
projectId || '',
|
|
122
120
|
streamId,
|
|
123
|
-
|
|
121
|
+
{ cols, rows }
|
|
124
122
|
);
|
|
125
123
|
|
|
126
124
|
// Broadcast initial ready event (frontend filters by sessionId)
|
|
@@ -179,20 +177,17 @@ export const sessionHandler = createRouter()
|
|
|
179
177
|
|
|
180
178
|
debug.log('terminal', `✅ Added fresh listeners to PTY session ${sessionId}`);
|
|
181
179
|
|
|
182
|
-
// Replay
|
|
183
|
-
// The
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
timestamp: new Date().toISOString()
|
|
194
|
-
});
|
|
195
|
-
}
|
|
180
|
+
// Replay serialized terminal state for reconnection (e.g., after browser refresh)
|
|
181
|
+
// The headless xterm preserves full terminal state including clear/scrollback
|
|
182
|
+
const serializedOutput = terminalStreamManager.getSerializedOutput(registeredStreamId);
|
|
183
|
+
if (serializedOutput) {
|
|
184
|
+
debug.log('terminal', `📜 Replaying serialized terminal state for session ${sessionId}`);
|
|
185
|
+
ws.emit.project(projectId, 'terminal:output', {
|
|
186
|
+
sessionId,
|
|
187
|
+
content: serializedOutput,
|
|
188
|
+
projectId,
|
|
189
|
+
timestamp: new Date().toISOString()
|
|
190
|
+
});
|
|
196
191
|
}
|
|
197
192
|
|
|
198
193
|
// Broadcast terminal tab created to all project users
|
|
@@ -216,6 +211,20 @@ export const sessionHandler = createRouter()
|
|
|
216
211
|
};
|
|
217
212
|
})
|
|
218
213
|
|
|
214
|
+
// Clear headless terminal buffer (sync with frontend clear)
|
|
215
|
+
.http('terminal:clear', {
|
|
216
|
+
data: t.Object({
|
|
217
|
+
sessionId: t.String()
|
|
218
|
+
}),
|
|
219
|
+
response: t.Object({
|
|
220
|
+
sessionId: t.String()
|
|
221
|
+
})
|
|
222
|
+
}, async ({ data }) => {
|
|
223
|
+
const { sessionId } = data;
|
|
224
|
+
terminalStreamManager.clearHeadlessTerminal(sessionId);
|
|
225
|
+
return { sessionId };
|
|
226
|
+
})
|
|
227
|
+
|
|
219
228
|
// Resize terminal viewport
|
|
220
229
|
.http('terminal:resize', {
|
|
221
230
|
data: t.Object({
|
|
@@ -239,6 +248,9 @@ export const sessionHandler = createRouter()
|
|
|
239
248
|
throw new Error('No active PTY session found');
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
// Keep headless terminal in sync with PTY dimensions
|
|
252
|
+
terminalStreamManager.resizeHeadlessTerminal(sessionId, cols, rows);
|
|
253
|
+
|
|
242
254
|
return { sessionId, cols, rows };
|
|
243
255
|
})
|
|
244
256
|
|
|
@@ -309,6 +321,12 @@ export const sessionHandler = createRouter()
|
|
|
309
321
|
|
|
310
322
|
debug.log('terminal', `💀 [kill-session] Successfully killed PTY session: ${sessionId} (PID: ${pid})`);
|
|
311
323
|
|
|
324
|
+
// Clean up stream and headless terminal
|
|
325
|
+
const stream = terminalStreamManager.getStreamBySession(sessionId);
|
|
326
|
+
if (stream) {
|
|
327
|
+
terminalStreamManager.removeStream(stream.streamId);
|
|
328
|
+
}
|
|
329
|
+
|
|
312
330
|
// Broadcast terminal tab closed to all project users
|
|
313
331
|
const projectId = ws.getProjectId(conn);
|
|
314
332
|
ws.emit.project(projectId, 'terminal:tab-closed', {
|