@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.
- package/backend/index.ts +28 -10
- package/backend/lib/chat/stream-manager.ts +130 -10
- package/backend/lib/database/queries/message-queries.ts +47 -0
- package/backend/lib/engine/adapters/claude/stream.ts +65 -1
- package/backend/lib/engine/adapters/opencode/message-converter.ts +35 -77
- package/backend/lib/engine/types.ts +6 -0
- package/backend/lib/files/file-operations.ts +2 -2
- package/backend/lib/files/file-reading.ts +2 -2
- package/backend/lib/files/path-browsing.ts +2 -2
- package/backend/lib/terminal/pty-session-manager.ts +1 -1
- package/backend/lib/terminal/shell-utils.ts +4 -4
- package/backend/lib/terminal/stream-manager.ts +6 -3
- package/backend/ws/chat/background.ts +3 -0
- package/backend/ws/chat/stream.ts +43 -1
- package/backend/ws/terminal/session.ts +48 -0
- package/bin/clopen.ts +10 -0
- package/bun.lock +258 -383
- package/frontend/lib/components/chat/ChatInterface.svelte +8 -1
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +20 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +3 -15
- package/frontend/lib/components/chat/formatters/Tools.svelte +15 -8
- package/frontend/lib/components/chat/input/ChatInput.svelte +70 -21
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +23 -11
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +1 -1
- package/frontend/lib/components/chat/message/ChatMessage.svelte +13 -1
- package/frontend/lib/components/chat/message/ChatMessages.svelte +2 -2
- package/frontend/lib/components/chat/message/DateSeparator.svelte +1 -1
- package/frontend/lib/components/chat/message/MessageBubble.svelte +2 -2
- package/frontend/lib/components/chat/message/MessageHeader.svelte +14 -12
- package/frontend/lib/components/chat/tools/AgentTool.svelte +95 -0
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +396 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +9 -4
- package/frontend/lib/components/chat/tools/EnterPlanModeTool.svelte +24 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +4 -7
- package/frontend/lib/components/chat/tools/{KillShellTool.svelte → TaskStopTool.svelte} +6 -6
- package/frontend/lib/components/chat/tools/index.ts +5 -2
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +7 -2
- package/frontend/lib/components/history/HistoryModal.svelte +13 -5
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +2 -1
- package/frontend/lib/components/workspace/MobileNavigator.svelte +2 -1
- package/frontend/lib/services/chat/chat.service.ts +146 -12
- package/frontend/lib/services/terminal/project.service.ts +65 -10
- package/frontend/lib/services/terminal/terminal.service.ts +19 -0
- package/frontend/lib/stores/core/app.svelte.ts +77 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +10 -0
- package/frontend/lib/utils/chat/message-grouper.ts +94 -12
- package/frontend/lib/utils/chat/message-processor.ts +37 -4
- package/frontend/lib/utils/chat/tool-handler.ts +96 -5
- package/package.json +4 -5
- package/shared/constants/engines.ts +1 -1
- package/shared/types/database/schema.ts +1 -0
- package/shared/types/messaging/index.ts +15 -13
- package/shared/types/messaging/tool.ts +185 -361
- 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
|
-
//
|
|
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
|
*
|