@myrialabs/clopen 0.0.7 → 0.1.1

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 (54) hide show
  1. package/backend/index.ts +28 -10
  2. package/backend/lib/chat/stream-manager.ts +130 -10
  3. package/backend/lib/database/queries/message-queries.ts +47 -0
  4. package/backend/lib/engine/adapters/claude/stream.ts +65 -1
  5. package/backend/lib/engine/adapters/opencode/message-converter.ts +35 -77
  6. package/backend/lib/engine/types.ts +6 -0
  7. package/backend/lib/files/file-operations.ts +2 -2
  8. package/backend/lib/files/file-reading.ts +2 -2
  9. package/backend/lib/files/path-browsing.ts +2 -2
  10. package/backend/lib/terminal/pty-session-manager.ts +1 -1
  11. package/backend/lib/terminal/shell-utils.ts +4 -4
  12. package/backend/lib/terminal/stream-manager.ts +6 -3
  13. package/backend/ws/chat/background.ts +3 -0
  14. package/backend/ws/chat/stream.ts +43 -1
  15. package/backend/ws/terminal/session.ts +48 -0
  16. package/bin/clopen.ts +10 -0
  17. package/bun.lock +258 -383
  18. package/frontend/lib/components/chat/ChatInterface.svelte +8 -1
  19. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +20 -0
  20. package/frontend/lib/components/chat/formatters/TextMessage.svelte +3 -15
  21. package/frontend/lib/components/chat/formatters/Tools.svelte +15 -8
  22. package/frontend/lib/components/chat/input/ChatInput.svelte +70 -21
  23. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +23 -11
  24. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +1 -1
  25. package/frontend/lib/components/chat/message/ChatMessage.svelte +13 -1
  26. package/frontend/lib/components/chat/message/ChatMessages.svelte +2 -2
  27. package/frontend/lib/components/chat/message/DateSeparator.svelte +1 -1
  28. package/frontend/lib/components/chat/message/MessageBubble.svelte +2 -2
  29. package/frontend/lib/components/chat/message/MessageHeader.svelte +14 -12
  30. package/frontend/lib/components/chat/tools/AgentTool.svelte +95 -0
  31. package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +396 -0
  32. package/frontend/lib/components/chat/tools/BashTool.svelte +9 -4
  33. package/frontend/lib/components/chat/tools/EnterPlanModeTool.svelte +24 -0
  34. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +4 -7
  35. package/frontend/lib/components/chat/tools/{KillShellTool.svelte → TaskStopTool.svelte} +6 -6
  36. package/frontend/lib/components/chat/tools/index.ts +5 -2
  37. package/frontend/lib/components/checkpoint/TimelineModal.svelte +7 -2
  38. package/frontend/lib/components/history/HistoryModal.svelte +13 -5
  39. package/frontend/lib/components/workspace/DesktopNavigator.svelte +2 -1
  40. package/frontend/lib/components/workspace/MobileNavigator.svelte +2 -1
  41. package/frontend/lib/services/chat/chat.service.ts +146 -12
  42. package/frontend/lib/services/terminal/project.service.ts +65 -10
  43. package/frontend/lib/services/terminal/terminal.service.ts +19 -0
  44. package/frontend/lib/stores/core/app.svelte.ts +77 -0
  45. package/frontend/lib/stores/features/terminal.svelte.ts +10 -0
  46. package/frontend/lib/utils/chat/message-grouper.ts +94 -12
  47. package/frontend/lib/utils/chat/message-processor.ts +37 -4
  48. package/frontend/lib/utils/chat/tool-handler.ts +96 -5
  49. package/package.json +4 -5
  50. package/shared/constants/engines.ts +1 -1
  51. package/shared/types/database/schema.ts +1 -0
  52. package/shared/types/messaging/index.ts +15 -13
  53. package/shared/types/messaging/tool.ts +185 -361
  54. package/shared/utils/message-formatter.ts +1 -0
@@ -102,7 +102,7 @@ export async function getShellConfig(preferGitBash = false): Promise<{
102
102
  // For Windows, always use PowerShell as the primary shell
103
103
  // PowerShell is available on all modern Windows systems
104
104
  return {
105
- shell: 'powershell',
105
+ shell: 'powershell.exe',
106
106
  args: (command: string) => ['-NoProfile', '-Command', command],
107
107
  name: 'PowerShell',
108
108
  isUnixLike: false
@@ -189,7 +189,7 @@ export function createPty(shell: string, args: string[], cwd: string, terminalSi
189
189
 
190
190
  if (isWindows) {
191
191
  // Windows: Always use PowerShell
192
- if (shell === 'powershell') {
192
+ if (shell === 'powershell' || shell === 'powershell.exe') {
193
193
  // Extract the actual command from args
194
194
  let actualCommand = args.join(' ');
195
195
  if (args.length >= 2 && (args[0] === '-Command' || args[0] === '-NoProfile')) {
@@ -201,7 +201,7 @@ export function createPty(shell: string, args: string[], cwd: string, terminalSi
201
201
  actualCommand = args[args.length - 1];
202
202
  }
203
203
  }
204
- return spawn('powershell', ['-NoProfile', '-NoLogo', '-Command', actualCommand], {
204
+ return spawn('powershell.exe', ['-NoProfile', '-NoLogo', '-Command', actualCommand], {
205
205
  name: 'xterm-256color',
206
206
  cols,
207
207
  rows,
@@ -211,7 +211,7 @@ export function createPty(shell: string, args: string[], cwd: string, terminalSi
211
211
  }
212
212
 
213
213
  // Default to PowerShell if shell is not recognized
214
- return spawn('powershell', ['-NoProfile', '-NoLogo', '-Command', args.join(' ') || 'Write-Host "Terminal ready"'], {
214
+ return spawn('powershell.exe', ['-NoProfile', '-NoLogo', '-Command', args.join(' ') || 'Write-Host "Terminal ready"'], {
215
215
  name: 'xterm-256color',
216
216
  cols,
217
217
  rows,
@@ -49,17 +49,20 @@ class TerminalStreamManager {
49
49
  ): string {
50
50
  // Check if there's already a stream for this session
51
51
  const existingStreamId = this.sessionToStream.get(sessionId);
52
+ let preservedOutput: string[] = [];
52
53
  if (existingStreamId) {
53
54
  const existingStream = this.streams.get(existingStreamId);
54
55
  if (existingStream) {
55
- // Clean up existing stream first
56
56
  if (existingStream.pty && existingStream.pty !== pty) {
57
- // If it's a different PTY, kill the old one
57
+ // Different PTY, kill the old one
58
58
  try {
59
59
  existingStream.pty.kill();
60
60
  } catch (error) {
61
61
  // Ignore error if PTY already killed
62
62
  }
63
+ } else if (existingStream.pty === pty) {
64
+ // Same PTY (reconnection after browser refresh) - preserve output buffer
65
+ preservedOutput = [...existingStream.output];
63
66
  }
64
67
  // Remove the old stream
65
68
  this.streams.delete(existingStreamId);
@@ -79,7 +82,7 @@ class TerminalStreamManager {
79
82
  workingDirectory,
80
83
  projectPath,
81
84
  projectId,
82
- output: [],
85
+ output: preservedOutput,
83
86
  processId: pty.pid,
84
87
  outputStartIndex: outputStartIndex || 0
85
88
  };
@@ -82,6 +82,7 @@ export const backgroundHandler = createRouter()
82
82
  processId: t.String(),
83
83
  messages: t.Array(t.Any()),
84
84
  currentPartialText: t.Optional(t.String()),
85
+ currentReasoningText: t.Optional(t.String()),
85
86
  error: t.Optional(t.String()),
86
87
  startedAt: t.String(),
87
88
  completedAt: t.Optional(t.String())
@@ -106,6 +107,7 @@ export const backgroundHandler = createRouter()
106
107
  processId: '',
107
108
  messages: [],
108
109
  currentPartialText: undefined,
110
+ currentReasoningText: undefined,
109
111
  error: undefined,
110
112
  startedAt: new Date().toISOString(),
111
113
  completedAt: new Date().toISOString()
@@ -122,6 +124,7 @@ export const backgroundHandler = createRouter()
122
124
  processId: streamState.processId,
123
125
  messages,
124
126
  currentPartialText: streamState.currentPartialText,
127
+ currentReasoningText: streamState.currentReasoningText,
125
128
  error: streamState.error,
126
129
  startedAt: streamState.startedAt.toISOString(),
127
130
  completedAt: streamState.completedAt?.toISOString()
@@ -13,7 +13,7 @@ import { streamManager, type StreamEvent } from '../../lib/chat/stream-manager';
13
13
  import { debug } from '$shared/utils/logger';
14
14
  import { ws } from '$backend/lib/utils/ws';
15
15
  import { broadcastPresence } from '../projects/status';
16
- import { sessionQueries } from '../../lib/database/queries';
16
+ import { sessionQueries, messageQueries } from '../../lib/database/queries';
17
17
 
18
18
  // ============================================================================
19
19
  // Global stream lifecycle handler (module-level, not per-connection)
@@ -28,6 +28,15 @@ streamManager.on('stream:lifecycle', (event: { status: string; streamId: string;
28
28
 
29
29
  debug.log('chat', `Stream lifecycle: ${status} for project ${projectId} session ${chatSessionId}`);
30
30
 
31
+ // Mark any tool_use blocks that never got a tool_result as interrupted (persisted to DB)
32
+ if (chatSessionId) {
33
+ try {
34
+ messageQueries.markInterruptedMessages(chatSessionId);
35
+ } catch (err) {
36
+ debug.error('chat', 'Failed to mark interrupted messages:', err);
37
+ }
38
+ }
39
+
31
40
  // Notify all project members (cross-project notification for sound + push)
32
41
  ws.emit.projectMembers(projectId, 'chat:stream-finished', {
33
42
  projectId,
@@ -360,6 +369,39 @@ export const streamHandler = createRouter()
360
369
  }
361
370
  })
362
371
 
372
+ // Handle AskUserQuestion answer from user
373
+ .on('chat:ask-user-answer', {
374
+ data: t.Object({
375
+ chatSessionId: t.String(),
376
+ toolUseId: t.String(),
377
+ answers: t.Record(t.String(), t.String())
378
+ })
379
+ }, async ({ data, conn }) => {
380
+ const projectId = ws.getProjectId(conn);
381
+
382
+ try {
383
+ debug.log('chat', 'WS chat:ask-user-answer received:', {
384
+ chatSessionId: data.chatSessionId,
385
+ toolUseId: data.toolUseId,
386
+ answers: data.answers
387
+ });
388
+
389
+ const success = streamManager.resolveUserAnswer(
390
+ data.chatSessionId,
391
+ projectId,
392
+ data.toolUseId,
393
+ data.answers
394
+ );
395
+
396
+ if (!success) {
397
+ debug.warn('chat', 'Failed to resolve user answer for stream');
398
+ }
399
+ } catch (error) {
400
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
401
+ debug.error('chat', 'WS chat:ask-user-answer error:', errorMessage);
402
+ }
403
+ })
404
+
363
405
  // Cancel stream
364
406
  .on('chat:cancel', {
365
407
  data: t.Object({
@@ -179,6 +179,22 @@ export const sessionHandler = createRouter()
179
179
 
180
180
  debug.log('terminal', `✅ Added fresh listeners to PTY session ${sessionId}`);
181
181
 
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
+ }
196
+ }
197
+
182
198
  // Broadcast terminal tab created to all project users
183
199
  ws.emit.project(projectId, 'terminal:tab-created', {
184
200
  sessionId,
@@ -379,4 +395,36 @@ export const sessionHandler = createRouter()
379
395
  message: 'PTY not found'
380
396
  };
381
397
  }
398
+ })
399
+
400
+ // List active PTY sessions for a project
401
+ // Used after browser refresh to discover existing sessions
402
+ .http('terminal:list-sessions', {
403
+ data: t.Object({
404
+ projectId: t.String()
405
+ }),
406
+ response: t.Object({
407
+ sessions: t.Array(t.Object({
408
+ sessionId: t.String(),
409
+ pid: t.Number(),
410
+ cwd: t.String(),
411
+ createdAt: t.String(),
412
+ lastActivityAt: t.String()
413
+ }))
414
+ })
415
+ }, async ({ data }) => {
416
+ const { projectId } = data;
417
+
418
+ const allSessions = ptySessionManager.getAllSessions();
419
+ const projectSessions = allSessions
420
+ .filter(session => session.projectId === projectId)
421
+ .map(session => ({
422
+ sessionId: session.sessionId,
423
+ pid: session.pty.pid,
424
+ cwd: session.cwd,
425
+ createdAt: session.createdAt.toISOString(),
426
+ lastActivityAt: session.lastActivityAt.toISOString()
427
+ }));
428
+
429
+ return { sessions: projectSessions };
382
430
  });
package/bin/clopen.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ // Runtime guard — Bun only, reject Node.js and Deno
4
+ if (typeof globalThis.Bun === 'undefined') {
5
+ console.error('\x1b[31mError: Clopen requires Bun runtime.\x1b[0m');
6
+ console.error('Node.js and Deno are not supported.');
7
+ console.error('');
8
+ console.error('Install Bun: https://bun.sh');
9
+ console.error('Then run: bun clopen');
10
+ process.exit(1);
11
+ }
12
+
3
13
  /**
4
14
  * Clopen CLI Entry Point
5
15
  *