@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.
Files changed (36) hide show
  1. package/backend/chat/stream-manager.ts +3 -0
  2. package/backend/engine/adapters/claude/stream.ts +2 -1
  3. package/backend/engine/types.ts +9 -0
  4. package/backend/mcp/config.ts +32 -6
  5. package/backend/snapshot/snapshot-service.ts +9 -7
  6. package/backend/terminal/stream-manager.ts +106 -155
  7. package/backend/ws/projects/crud.ts +3 -3
  8. package/backend/ws/snapshot/timeline.ts +6 -2
  9. package/backend/ws/terminal/persistence.ts +19 -33
  10. package/backend/ws/terminal/session.ts +37 -19
  11. package/bin/clopen.ts +376 -99
  12. package/bun.lock +6 -0
  13. package/frontend/components/chat/input/ChatInput.svelte +8 -0
  14. package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
  15. package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
  16. package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
  17. package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
  18. package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
  19. package/frontend/components/common/overlay/Dialog.svelte +2 -2
  20. package/frontend/components/git/ChangesSection.svelte +104 -13
  21. package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
  22. package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
  23. package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
  24. package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
  25. package/frontend/components/terminal/Terminal.svelte +5 -1
  26. package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
  27. package/frontend/services/chat/chat.service.ts +52 -11
  28. package/frontend/services/terminal/project.service.ts +4 -60
  29. package/frontend/services/terminal/terminal.service.ts +18 -27
  30. package/frontend/stores/core/sessions.svelte.ts +6 -0
  31. package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
  32. package/frontend/stores/ui/theme.svelte.ts +11 -11
  33. package/frontend/stores/ui/workspace.svelte.ts +1 -1
  34. package/index.html +2 -2
  35. package/package.json +4 -2
  36. 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.Array(t.String()),
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, fromIndex = 0 } = data;
50
+ const { sessionId, streamId } = data;
54
51
 
55
- // Try to get output from stream manager (memory or cache)
56
- let output: string[] = [];
52
+ // Get serialized terminal state from headless xterm
53
+ let output = '';
57
54
 
58
55
  if (streamId) {
59
- // If streamId is provided, get output from that specific stream
60
- output = terminalStreamManager.getOutput(streamId, fromIndex);
56
+ output = terminalStreamManager.getSerializedOutput(streamId);
61
57
  } else {
62
- // Otherwise try to load cached output for the session
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, fromIndex = 0 } = data;
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
- // Broadcast missed output (frontend filters by sessionId for one-time replay)
106
- const existingOutput = terminalStreamManager.getOutput(streamId, fromIndex);
107
-
108
- if (existingOutput.length > 0) {
109
- for (const output of existingOutput) {
110
- ws.emit.project(projectId, 'terminal:output', {
111
- sessionId,
112
- content: output,
113
- timestamp: new Date().toISOString()
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
- outputStartIndex
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 historical output for reconnection (e.g., after browser refresh)
183
- // The stream preserves output from the old stream when reconnecting to the same PTY.
184
- // Replay from outputStartIndex so frontend receives all output it doesn't have yet.
185
- const historicalOutput = terminalStreamManager.getOutput(registeredStreamId, outputStartIndex);
186
- if (historicalOutput.length > 0) {
187
- debug.log('terminal', `📜 Replaying ${historicalOutput.length} historical output entries for session ${sessionId}`);
188
- for (const output of historicalOutput) {
189
- ws.emit.project(projectId, 'terminal:output', {
190
- sessionId,
191
- content: output,
192
- projectId,
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', {