mstro-app 0.3.4 → 0.3.6
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/bin/mstro.js +15 -2
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +5 -10
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +4 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +39 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +2 -13
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +19 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +48 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.d.ts +44 -0
- package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -0
- package/dist/server/services/websocket/file-upload-handler.js +185 -0
- package/dist/server/services/websocket/file-upload-handler.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +3 -3
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +40 -2
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +3 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +4 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +31 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +69 -20
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +6 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +16 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +33 -24
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.d.ts +4 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +35 -4
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker.ts +5 -11
- package/server/cli/improvisation-session-manager.ts +42 -1
- package/server/services/platform.ts +2 -12
- package/server/services/terminal/pty-manager.ts +57 -2
- package/server/services/websocket/file-explorer-handlers.ts +16 -1
- package/server/services/websocket/file-upload-handler.ts +259 -0
- package/server/services/websocket/git-handlers.ts +3 -3
- package/server/services/websocket/git-worktree-handlers.ts +47 -3
- package/server/services/websocket/handler-context.ts +3 -0
- package/server/services/websocket/handler.ts +33 -0
- package/server/services/websocket/session-handlers.ts +79 -20
- package/server/services/websocket/session-registry.ts +18 -0
- package/server/services/websocket/tab-handlers.ts +44 -23
- package/server/services/websocket/terminal-handlers.ts +40 -4
- package/server/services/websocket/types.ts +15 -2
|
@@ -15,7 +15,7 @@ export interface WSContext {
|
|
|
15
15
|
_ws?: unknown;
|
|
16
16
|
}
|
|
17
17
|
export interface WebSocketMessage {
|
|
18
|
-
type: 'execute' | 'cancel' | 'getHistory' | 'getSessions' | 'getSessionsCount' | 'deleteSession' | 'getSessionById' | 'clearHistory' | 'searchHistory' | 'new' | 'autocomplete' | 'readFile' | 'ping' | 'initTab' | 'resumeSession' | 'approve' | 'reject' | 'recordSelection' | 'requestNotificationSummary' | 'terminalInit' | 'terminalReconnect' | 'terminalList' | 'terminalInput' | 'terminalResize' | 'terminalClose' | 'listDirectory' | 'writeFile' | 'createFile' | 'createDirectory' | 'deleteFile' | 'renameFile' | 'notifyFileOpened' | 'searchFileContents' | 'cancelSearch' | 'findDefinition' | 'gitStatus' | 'gitStage' | 'gitUnstage' | 'gitCommit' | 'gitCommitWithAI' | 'gitPush' | 'gitPull' | 'gitLog' | 'gitDiscoverRepos' | 'gitSetDirectory' | 'gitGetRemoteInfo' | 'gitCreatePR' | 'gitGeneratePRDescription' | 'gitListBranches' | 'gitCheckout' | 'gitCreateBranch' | 'gitDeleteBranch' | 'gitDiff' | 'gitListTags' | 'gitCreateTag' | 'gitPushTag' | 'gitWorktreeList' | 'gitWorktreeCreate' | 'gitWorktreeRemove' | 'tabWorktreeSwitch' | 'gitWorktreePush' | 'gitWorktreeCreatePR' | 'gitMergePreview' | 'gitWorktreeMerge' | 'gitMergeAbort' | 'gitMergeComplete' | 'getActiveTabs' | 'createTab' | 'reorderTabs' | 'syncTabMeta' | 'syncPromptText' | 'removeTab' | 'markTabViewed' | 'getSettings' | 'updateSettings';
|
|
18
|
+
type: 'execute' | 'cancel' | 'getHistory' | 'getSessions' | 'getSessionsCount' | 'deleteSession' | 'getSessionById' | 'clearHistory' | 'searchHistory' | 'new' | 'autocomplete' | 'readFile' | 'ping' | 'initTab' | 'resumeSession' | 'approve' | 'reject' | 'recordSelection' | 'requestNotificationSummary' | 'terminalInit' | 'terminalReconnect' | 'terminalList' | 'terminalInput' | 'terminalResize' | 'terminalClose' | 'listDirectory' | 'writeFile' | 'createFile' | 'createDirectory' | 'deleteFile' | 'renameFile' | 'notifyFileOpened' | 'searchFileContents' | 'cancelSearch' | 'findDefinition' | 'gitStatus' | 'gitStage' | 'gitUnstage' | 'gitCommit' | 'gitCommitWithAI' | 'gitPush' | 'gitPull' | 'gitLog' | 'gitDiscoverRepos' | 'gitSetDirectory' | 'gitGetRemoteInfo' | 'gitCreatePR' | 'gitGeneratePRDescription' | 'gitListBranches' | 'gitCheckout' | 'gitCreateBranch' | 'gitDeleteBranch' | 'gitDiff' | 'gitListTags' | 'gitCreateTag' | 'gitPushTag' | 'gitWorktreeList' | 'gitWorktreeCreate' | 'gitWorktreeCreateAndAssign' | 'gitWorktreeRemove' | 'tabWorktreeSwitch' | 'gitWorktreePush' | 'gitWorktreeCreatePR' | 'gitMergePreview' | 'gitWorktreeMerge' | 'gitMergeAbort' | 'gitMergeComplete' | 'getActiveTabs' | 'createTab' | 'reorderTabs' | 'syncTabMeta' | 'syncPromptText' | 'removeTab' | 'markTabViewed' | 'getSettings' | 'updateSettings' | 'fileUploadStart' | 'fileUploadChunk' | 'fileUploadComplete' | 'fileUploadCancel';
|
|
19
19
|
tabId?: string;
|
|
20
20
|
terminalId?: string;
|
|
21
21
|
data?: any;
|
|
@@ -23,7 +23,7 @@ export interface WebSocketMessage {
|
|
|
23
23
|
_permission?: 'control' | 'view';
|
|
24
24
|
}
|
|
25
25
|
export interface WebSocketResponse {
|
|
26
|
-
type: 'output' | 'thinking' | 'movementStart' | 'movementComplete' | 'movementError' | 'sessionUpdate' | 'history' | 'sessions' | 'sessionsCount' | 'sessionDeleted' | 'sessionData' | 'historyCleared' | 'searchResults' | 'newSession' | 'autocomplete' | 'fileContent' | 'error' | 'pong' | 'tabInitialized' | 'approvalRequired' | 'toolUse' | 'streamingTokens' | 'notificationSummary' | 'terminalOutput' | 'terminalReady' | 'terminalExit' | 'terminalError' | 'terminalList' | 'directoryListing' | 'fileWritten' | 'fileCreated' | 'directoryCreated' | 'fileDeleted' | 'fileRenamed' | 'fileOpened' | 'fileContentChanged' | 'contentSearchResults' | 'contentSearchComplete' | 'contentSearchError' | 'definitionResult' | 'terminalCreated' | 'terminalClosed' | 'gitStatus' | 'gitStaged' | 'gitUnstaged' | 'gitCommitted' | 'gitCommitMessage' | 'gitPushed' | 'gitPulled' | 'gitLog' | 'gitError' | 'gitReposDiscovered' | 'gitDirectorySet' | 'gitRemoteInfo' | 'gitPRCreated' | 'gitPRDescription' | 'gitBranchList' | 'gitCheckedOut' | 'gitBranchCreated' | 'gitBranchDeleted' | 'gitDiffResult' | 'gitTagList' | 'gitTagCreated' | 'gitTagPushed' | 'gitWorktreeListResult' | 'gitWorktreeCreated' | 'gitWorktreeRemoved' | 'tabWorktreeSwitched' | 'gitWorktreePushed' | 'gitWorktreePRCreated' | 'gitMergePreviewResult' | 'gitWorktreeMergeResult' | 'gitMergeAborted' | 'gitMergeCompleted' | 'activeTabs' | 'tabCreated' | 'tabRemoved' | 'tabRenamed' | 'tabsReordered' | 'promptTextSync' | 'tabViewed' | 'tabStateChanged' | 'settings' | 'settingsUpdated';
|
|
26
|
+
type: 'output' | 'thinking' | 'movementStart' | 'movementComplete' | 'movementError' | 'sessionUpdate' | 'history' | 'sessions' | 'sessionsCount' | 'sessionDeleted' | 'sessionData' | 'historyCleared' | 'searchResults' | 'newSession' | 'autocomplete' | 'fileContent' | 'error' | 'pong' | 'tabInitialized' | 'approvalRequired' | 'toolUse' | 'streamingTokens' | 'notificationSummary' | 'terminalOutput' | 'terminalReady' | 'terminalExit' | 'terminalError' | 'terminalList' | 'directoryListing' | 'fileWritten' | 'fileCreated' | 'directoryCreated' | 'fileDeleted' | 'fileRenamed' | 'fileOpened' | 'fileContentChanged' | 'contentSearchResults' | 'contentSearchComplete' | 'contentSearchError' | 'definitionResult' | 'fileError' | 'terminalScrollback' | 'terminalCreated' | 'terminalClosed' | 'gitStatus' | 'gitStaged' | 'gitUnstaged' | 'gitCommitted' | 'gitCommitMessage' | 'gitPushed' | 'gitPulled' | 'gitLog' | 'gitError' | 'gitReposDiscovered' | 'gitDirectorySet' | 'gitRemoteInfo' | 'gitPRCreated' | 'gitPRDescription' | 'gitBranchList' | 'gitCheckedOut' | 'gitBranchCreated' | 'gitBranchDeleted' | 'gitDiffResult' | 'gitTagList' | 'gitTagCreated' | 'gitTagPushed' | 'gitWorktreeListResult' | 'gitWorktreeCreated' | 'gitWorktreeCreatedAndAssigned' | 'gitWorktreeRemoved' | 'tabWorktreeSwitched' | 'gitWorktreePushed' | 'gitWorktreePRCreated' | 'gitMergePreviewResult' | 'gitWorktreeMergeResult' | 'gitMergeAborted' | 'gitMergeCompleted' | 'activeTabs' | 'tabCreated' | 'tabRemoved' | 'tabRenamed' | 'tabsReordered' | 'promptTextSync' | 'tabViewed' | 'tabStateChanged' | 'settings' | 'settingsUpdated' | 'fileUploadAck' | 'fileUploadReady' | 'fileUploadError';
|
|
27
27
|
tabId?: string;
|
|
28
28
|
terminalId?: string;
|
|
29
29
|
data?: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/types.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACjC,KAAK,IAAI,IAAI,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAElB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EACA,SAAS,GACT,QAAQ,GACR,YAAY,GACZ,aAAa,GACb,kBAAkB,GAClB,eAAe,GACf,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,KAAK,GACL,cAAc,GACd,UAAU,GACV,MAAM,GACN,SAAS,GACT,eAAe,GACf,SAAS,GACT,QAAQ,GACR,iBAAiB,GACjB,4BAA4B,GAC5B,cAAc,GACd,mBAAmB,GACnB,cAAc,GACd,eAAe,GACf,gBAAgB,GAChB,eAAe,GAEf,eAAe,GACf,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,YAAY,GACZ,YAAY,GACZ,kBAAkB,GAClB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAEhB,WAAW,GACX,UAAU,GACV,YAAY,GACZ,WAAW,GACX,iBAAiB,GACjB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,kBAAkB,GAClB,iBAAiB,GACjB,kBAAkB,GAClB,aAAa,GACb,0BAA0B,GAE1B,iBAAiB,GACjB,aAAa,GACb,iBAAiB,GACjB,iBAAiB,GAEjB,SAAS,GAET,aAAa,GACb,cAAc,GACd,YAAY,GAEZ,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,iBAAiB,GACjB,qBAAqB,GAErB,iBAAiB,GACjB,kBAAkB,GAClB,eAAe,GACf,kBAAkB,GAElB,eAAe,GACf,WAAW,GACX,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,WAAW,GACX,eAAe,GAEf,aAAa,GACb,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../server/services/websocket/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -137,15 +137,10 @@ async function runStallAssessment(
|
|
|
137
137
|
/** Regex matching Claude Code's internal tool timeout messages */
|
|
138
138
|
const NATIVE_TIMEOUT_PATTERN = /^(\w+) timed out — (continuing|retrying) with (\d+) results? preserved$/;
|
|
139
139
|
|
|
140
|
-
/** Quick prefix check: does incomplete text look like it might be a timeout?
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const NATIVE_TIMEOUT_TOOL_NAMES = new Set([
|
|
145
|
-
'Read', 'Grep', 'Glob', 'Edit', 'Write', 'Bash',
|
|
146
|
-
'WebFetch', 'WebSearch', 'Task', 'TodoRead', 'TodoWrite',
|
|
147
|
-
'NotebookEdit', 'MultiEdit',
|
|
148
|
-
]);
|
|
140
|
+
/** Quick prefix check: does incomplete text look like it might be a timeout?
|
|
141
|
+
* Matches any capitalized tool name followed by " timed" — no hardcoded set
|
|
142
|
+
* needed because the full NATIVE_TIMEOUT_PATTERN validates on the next chunk. */
|
|
143
|
+
const TIMEOUT_PREFIX_PATTERN = /^[A-Z]\w* timed/;
|
|
149
144
|
|
|
150
145
|
interface NativeTimeoutEvent {
|
|
151
146
|
toolName: string;
|
|
@@ -203,8 +198,7 @@ class NativeTimeoutDetector {
|
|
|
203
198
|
|
|
204
199
|
// Handle incomplete trailing text
|
|
205
200
|
if (incomplete) {
|
|
206
|
-
|
|
207
|
-
if (prefixMatch && NATIVE_TIMEOUT_TOOL_NAMES.has(prefixMatch[1])) {
|
|
201
|
+
if (TIMEOUT_PREFIX_PATTERN.test(incomplete)) {
|
|
208
202
|
// Looks like the start of a timeout message — hold it
|
|
209
203
|
this.lineBuffer = incomplete;
|
|
210
204
|
} else {
|
|
@@ -283,6 +283,13 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
283
283
|
|
|
284
284
|
const paths: string[] = [];
|
|
285
285
|
for (const attachment of attachments) {
|
|
286
|
+
// Pre-uploaded files are already on disk from chunked upload
|
|
287
|
+
if ((attachment as FileAttachment & { _preUploaded?: boolean })._preUploaded) {
|
|
288
|
+
if (existsSync(attachment.filePath)) {
|
|
289
|
+
paths.push(attachment.filePath);
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
286
293
|
const filePath = join(attachDir, attachment.fileName);
|
|
287
294
|
try {
|
|
288
295
|
// All paste content arrives as base64 — decode to binary
|
|
@@ -485,16 +492,50 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
485
492
|
return result;
|
|
486
493
|
}
|
|
487
494
|
|
|
495
|
+
/** MIME types that the Claude API can accept as image content blocks */
|
|
496
|
+
private static readonly SUPPORTED_IMAGE_MIMES = new Set([
|
|
497
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
/** Hydrate pre-uploaded images from disk and downgrade unsupported formats */
|
|
501
|
+
private hydrateAndFilterAttachments(attachments: FileAttachment[]): void {
|
|
502
|
+
for (const attachment of attachments) {
|
|
503
|
+
// Pre-uploaded images need their content read from disk
|
|
504
|
+
const preUploaded = (attachment as FileAttachment & { _preUploaded?: boolean })._preUploaded;
|
|
505
|
+
if (preUploaded && attachment.isImage && !attachment.content && existsSync(attachment.filePath)) {
|
|
506
|
+
try {
|
|
507
|
+
attachment.content = readFileSync(attachment.filePath).toString('base64');
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.error(`Failed to read pre-uploaded image ${attachment.filePath}:`, err);
|
|
510
|
+
attachment.isImage = false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Downgrade unsupported image formats (SVG, BMP, TIFF, ICO, etc.) to text attachments
|
|
515
|
+
if (attachment.isImage) {
|
|
516
|
+
const mime = (attachment.mimeType || '').toLowerCase();
|
|
517
|
+
if (mime && !ImprovisationSessionManager.SUPPORTED_IMAGE_MIMES.has(mime)) {
|
|
518
|
+
attachment.isImage = false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
488
524
|
/** Prepare prompt with attachments and limit image count */
|
|
489
525
|
private preparePromptAndAttachments(
|
|
490
526
|
userPrompt: string,
|
|
491
527
|
attachments: FileAttachment[] | undefined,
|
|
492
528
|
): { prompt: string; imageAttachments: FileAttachment[] | undefined } {
|
|
529
|
+
if (attachments) {
|
|
530
|
+
this.hydrateAndFilterAttachments(attachments);
|
|
531
|
+
}
|
|
532
|
+
|
|
493
533
|
const diskPaths = attachments ? this.persistAttachments(attachments) : [];
|
|
494
534
|
const prompt = this.buildPromptWithAttachments(userPrompt, attachments, diskPaths);
|
|
495
535
|
|
|
496
536
|
const MAX_IMAGE_ATTACHMENTS = 20;
|
|
497
|
-
|
|
537
|
+
// Only include images that have valid content
|
|
538
|
+
const allImages = attachments?.filter(a => a.isImage && a.content);
|
|
498
539
|
let imageAttachments = allImages;
|
|
499
540
|
if (allImages && allImages.length > MAX_IMAGE_ATTACHMENTS) {
|
|
500
541
|
imageAttachments = allImages.slice(-MAX_IMAGE_ATTACHMENTS);
|
|
@@ -102,18 +102,8 @@ if (typeof WebSocket !== 'undefined') {
|
|
|
102
102
|
WebSocketImpl = WS as unknown as typeof WebSocket
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const envPath = join(MSTRO_DIR, '.env')
|
|
109
|
-
const content = readFileSync(envPath, 'utf-8')
|
|
110
|
-
const match = content.match(/^SERVER_URL=(.+)$/m)
|
|
111
|
-
if (match) return match[1].trim()
|
|
112
|
-
} catch {}
|
|
113
|
-
return 'https://api.mstro.app'
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
|
|
105
|
+
// PLATFORM_URL is set via --server / --dev flag in mstro.js
|
|
106
|
+
const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
|
|
117
107
|
|
|
118
108
|
interface ConnectionCallbacks {
|
|
119
109
|
onConnected?: (connectionId: string) => void
|
|
@@ -102,6 +102,47 @@ export function getPtyInstallInstructions(): string {
|
|
|
102
102
|
// Import type separately for type-checking (doesn't require the module to load)
|
|
103
103
|
type IPty = import('node-pty').IPty;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Fixed-size buffer that retains the most recent PTY output for replay on reconnect.
|
|
107
|
+
* Stores raw string chunks and evicts oldest data when the total exceeds maxBytes.
|
|
108
|
+
*/
|
|
109
|
+
class ScrollbackBuffer {
|
|
110
|
+
private chunks: string[] = [];
|
|
111
|
+
private totalLength = 0;
|
|
112
|
+
private maxBytes: number;
|
|
113
|
+
|
|
114
|
+
constructor(maxBytes: number) {
|
|
115
|
+
this.maxBytes = maxBytes;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
append(data: string): void {
|
|
119
|
+
this.chunks.push(data);
|
|
120
|
+
this.totalLength += data.length;
|
|
121
|
+
// Evict oldest chunks until under budget
|
|
122
|
+
while (this.totalLength > this.maxBytes && this.chunks.length > 1) {
|
|
123
|
+
const removed = this.chunks.shift()!;
|
|
124
|
+
this.totalLength -= removed.length;
|
|
125
|
+
}
|
|
126
|
+
// If a single chunk exceeds max, truncate from the front
|
|
127
|
+
if (this.totalLength > this.maxBytes && this.chunks.length === 1) {
|
|
128
|
+
const excess = this.totalLength - this.maxBytes;
|
|
129
|
+
this.chunks[0] = this.chunks[0].slice(excess);
|
|
130
|
+
this.totalLength = this.chunks[0].length;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getContents(): string {
|
|
135
|
+
return this.chunks.join('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clear(): void {
|
|
139
|
+
this.chunks = [];
|
|
140
|
+
this.totalLength = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const SCROLLBACK_MAX_BYTES = 256 * 1024; // 256KB
|
|
145
|
+
|
|
105
146
|
export interface PTYSession {
|
|
106
147
|
id: string;
|
|
107
148
|
pty: IPty;
|
|
@@ -117,6 +158,8 @@ export interface PTYSession {
|
|
|
117
158
|
// Output coalescing: buffer small chunks into fewer WS messages
|
|
118
159
|
_outputBuffer: string;
|
|
119
160
|
_outputTimer: ReturnType<typeof setTimeout> | null;
|
|
161
|
+
// Scrollback ring buffer for replay on reconnect
|
|
162
|
+
scrollback: ScrollbackBuffer;
|
|
120
163
|
}
|
|
121
164
|
|
|
122
165
|
/**
|
|
@@ -201,7 +244,7 @@ export class PTYManager extends EventEmitter {
|
|
|
201
244
|
rows: number = 24,
|
|
202
245
|
requestedShell?: string,
|
|
203
246
|
options?: { sandboxed?: boolean }
|
|
204
|
-
): { shell: string; cwd: string; isReconnect: boolean } {
|
|
247
|
+
): { shell: string; cwd: string; isReconnect: boolean; platform: string } {
|
|
205
248
|
// Check if node-pty is available
|
|
206
249
|
if (!pty) {
|
|
207
250
|
throw new Error(`PTY_NOT_AVAILABLE:${getPtyInstallInstructions()}`);
|
|
@@ -221,6 +264,7 @@ export class PTYManager extends EventEmitter {
|
|
|
221
264
|
shell: existingSession.shell,
|
|
222
265
|
cwd: existingSession.cwd,
|
|
223
266
|
isReconnect: true,
|
|
267
|
+
platform: platform(),
|
|
224
268
|
};
|
|
225
269
|
}
|
|
226
270
|
|
|
@@ -259,6 +303,7 @@ export class PTYManager extends EventEmitter {
|
|
|
259
303
|
rows,
|
|
260
304
|
_outputBuffer: '',
|
|
261
305
|
_outputTimer: null,
|
|
306
|
+
scrollback: new ScrollbackBuffer(SCROLLBACK_MAX_BYTES),
|
|
262
307
|
};
|
|
263
308
|
this.terminals.set(terminalId, session);
|
|
264
309
|
|
|
@@ -288,6 +333,7 @@ export class PTYManager extends EventEmitter {
|
|
|
288
333
|
};
|
|
289
334
|
|
|
290
335
|
ptyProcess.onData((data: string) => {
|
|
336
|
+
session.scrollback.append(data);
|
|
291
337
|
session.lastActivityAt = Date.now();
|
|
292
338
|
session._outputBuffer += data;
|
|
293
339
|
// Flush immediately if buffer exceeds high-water mark
|
|
@@ -310,7 +356,7 @@ export class PTYManager extends EventEmitter {
|
|
|
310
356
|
this.terminals.delete(terminalId);
|
|
311
357
|
});
|
|
312
358
|
|
|
313
|
-
return { shell: session.shell, cwd, isReconnect: false };
|
|
359
|
+
return { shell: session.shell, cwd, isReconnect: false, platform: platform() };
|
|
314
360
|
} catch (error: unknown) {
|
|
315
361
|
console.error(`[PTYManager] Failed to create terminal ${terminalId}:`, error);
|
|
316
362
|
this.emit('error', terminalId, error instanceof Error ? error.message : 'Failed to create terminal');
|
|
@@ -387,6 +433,15 @@ export class PTYManager extends EventEmitter {
|
|
|
387
433
|
}
|
|
388
434
|
}
|
|
389
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Get scrollback buffer contents for replay on reconnect
|
|
438
|
+
*/
|
|
439
|
+
getScrollback(terminalId: string): string | null {
|
|
440
|
+
const session = this.terminals.get(terminalId);
|
|
441
|
+
if (!session) return null;
|
|
442
|
+
return session.scrollback.getContents();
|
|
443
|
+
}
|
|
444
|
+
|
|
390
445
|
/**
|
|
391
446
|
* Get terminal session info
|
|
392
447
|
*/
|
|
@@ -74,7 +74,22 @@ export function handleFileExplorerMessage(ctx: HandlerContext, ws: WSContext, ms
|
|
|
74
74
|
cancelSearch: () => handleCancelSearch(ctx, tabId),
|
|
75
75
|
findDefinition: () => handleFindDefinition(ctx, ws, msg, tabId, workingDir),
|
|
76
76
|
};
|
|
77
|
-
handlers[msg.type]
|
|
77
|
+
const handler = handlers[msg.type];
|
|
78
|
+
if (!handler) return;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
handler();
|
|
82
|
+
} catch (error: unknown) {
|
|
83
|
+
// Send a domain-specific fileError so the web client can resolve pending
|
|
84
|
+
// promises instead of letting the generic handler send { type: 'error' }
|
|
85
|
+
// which no file-explorer listener handles (causing orphaned promises).
|
|
86
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
87
|
+
ctx.send(ws, {
|
|
88
|
+
type: 'fileError',
|
|
89
|
+
tabId,
|
|
90
|
+
data: { operation: msg.type, path: msg.data?.dirPath || msg.data?.filePath || '', error: errorMessage },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
function handleListDirectory(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Chunked File Upload Handler
|
|
6
|
+
*
|
|
7
|
+
* Receives files in chunks over WebSocket from remote web clients,
|
|
8
|
+
* writes them to .mstro/tmp/attachments/{tabId}/, and sends progress acks back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { WriteStream } from 'node:fs';
|
|
12
|
+
import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import type { WebSocketResponse, WSContext } from './types.js';
|
|
15
|
+
|
|
16
|
+
interface UploadState {
|
|
17
|
+
uploadId: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
fileSize: number;
|
|
20
|
+
mimeType: string;
|
|
21
|
+
isImage: boolean;
|
|
22
|
+
totalChunks: number;
|
|
23
|
+
receivedChunks: number;
|
|
24
|
+
filePath: string;
|
|
25
|
+
stream: WriteStream;
|
|
26
|
+
lastActivity: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Completed upload that's ready to be referenced in an execute message */
|
|
30
|
+
export interface CompletedUpload {
|
|
31
|
+
uploadId: string;
|
|
32
|
+
fileName: string;
|
|
33
|
+
filePath: string;
|
|
34
|
+
isImage: boolean;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
fileSize: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const UPLOAD_TIMEOUT_MS = 120_000; // 2 minutes idle timeout
|
|
40
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
41
|
+
|
|
42
|
+
export class FileUploadHandler {
|
|
43
|
+
private activeUploads = new Map<string, UploadState>();
|
|
44
|
+
private completedUploads = new Map<string, CompletedUpload[]>(); // tabId -> completed uploads
|
|
45
|
+
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
46
|
+
|
|
47
|
+
constructor(private workingDir: string) {
|
|
48
|
+
// Periodically clean up stale uploads
|
|
49
|
+
this.cleanupInterval = setInterval(() => this.cleanupStaleUploads(), 30_000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get completed uploads for a tab and clear them */
|
|
53
|
+
getAndClearCompletedUploads(tabId: string): CompletedUpload[] {
|
|
54
|
+
const uploads = this.completedUploads.get(tabId) || [];
|
|
55
|
+
this.completedUploads.delete(tabId);
|
|
56
|
+
return uploads;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Get completed uploads for a tab without clearing */
|
|
60
|
+
getCompletedUploads(tabId: string): CompletedUpload[] {
|
|
61
|
+
return this.completedUploads.get(tabId) || [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleUploadStart(
|
|
65
|
+
ws: WSContext,
|
|
66
|
+
send: (ws: WSContext, response: WebSocketResponse) => void,
|
|
67
|
+
tabId: string,
|
|
68
|
+
data: { uploadId: string; fileName: string; fileSize: number; mimeType: string; isImage: boolean; totalChunks: number }
|
|
69
|
+
): void {
|
|
70
|
+
const { uploadId, fileName, fileSize, mimeType, isImage, totalChunks } = data;
|
|
71
|
+
|
|
72
|
+
// Validate file size
|
|
73
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
74
|
+
send(ws, {
|
|
75
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
76
|
+
tabId,
|
|
77
|
+
data: { uploadId, error: `File too large: ${(fileSize / 1024 / 1024).toFixed(1)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit` }
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create attachment directory
|
|
83
|
+
const attachDir = join(this.workingDir, '.mstro', 'tmp', 'attachments', tabId);
|
|
84
|
+
if (!existsSync(attachDir)) {
|
|
85
|
+
mkdirSync(attachDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle duplicate file names
|
|
89
|
+
let targetFileName = fileName;
|
|
90
|
+
let counter = 1;
|
|
91
|
+
while (existsSync(join(attachDir, targetFileName))) {
|
|
92
|
+
const ext = fileName.lastIndexOf('.') !== -1 ? fileName.slice(fileName.lastIndexOf('.')) : '';
|
|
93
|
+
const base = fileName.lastIndexOf('.') !== -1 ? fileName.slice(0, fileName.lastIndexOf('.')) : fileName;
|
|
94
|
+
targetFileName = `${base}-${counter}${ext}`;
|
|
95
|
+
counter++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const filePath = join(attachDir, targetFileName);
|
|
99
|
+
const stream = createWriteStream(filePath);
|
|
100
|
+
|
|
101
|
+
const uploadState: UploadState = {
|
|
102
|
+
uploadId,
|
|
103
|
+
fileName: targetFileName,
|
|
104
|
+
fileSize,
|
|
105
|
+
mimeType,
|
|
106
|
+
isImage,
|
|
107
|
+
totalChunks,
|
|
108
|
+
receivedChunks: 0,
|
|
109
|
+
filePath,
|
|
110
|
+
stream,
|
|
111
|
+
lastActivity: Date.now(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.activeUploads.set(uploadId, uploadState);
|
|
115
|
+
|
|
116
|
+
// Send ack for start
|
|
117
|
+
send(ws, {
|
|
118
|
+
type: 'fileUploadAck' as WebSocketResponse['type'],
|
|
119
|
+
tabId,
|
|
120
|
+
data: { uploadId, chunkIndex: -1, status: 'ok' }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
handleUploadChunk(
|
|
125
|
+
ws: WSContext,
|
|
126
|
+
send: (ws: WSContext, response: WebSocketResponse) => void,
|
|
127
|
+
tabId: string,
|
|
128
|
+
data: { uploadId: string; chunkIndex: number; content: string }
|
|
129
|
+
): void {
|
|
130
|
+
const { uploadId, chunkIndex, content } = data;
|
|
131
|
+
const upload = this.activeUploads.get(uploadId);
|
|
132
|
+
|
|
133
|
+
if (!upload) {
|
|
134
|
+
send(ws, {
|
|
135
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
136
|
+
tabId,
|
|
137
|
+
data: { uploadId, error: 'Upload not found or expired' }
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const buffer = Buffer.from(content, 'base64');
|
|
144
|
+
upload.stream.write(buffer);
|
|
145
|
+
upload.receivedChunks++;
|
|
146
|
+
upload.lastActivity = Date.now();
|
|
147
|
+
|
|
148
|
+
send(ws, {
|
|
149
|
+
type: 'fileUploadAck' as WebSocketResponse['type'],
|
|
150
|
+
tabId,
|
|
151
|
+
data: { uploadId, chunkIndex, status: 'ok' }
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
send(ws, {
|
|
156
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
157
|
+
tabId,
|
|
158
|
+
data: { uploadId, error: `Chunk write failed: ${errorMsg}` }
|
|
159
|
+
});
|
|
160
|
+
this.cancelUpload(uploadId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
handleUploadComplete(
|
|
165
|
+
ws: WSContext,
|
|
166
|
+
send: (ws: WSContext, response: WebSocketResponse) => void,
|
|
167
|
+
tabId: string,
|
|
168
|
+
data: { uploadId: string }
|
|
169
|
+
): void {
|
|
170
|
+
const { uploadId } = data;
|
|
171
|
+
const upload = this.activeUploads.get(uploadId);
|
|
172
|
+
|
|
173
|
+
if (!upload) {
|
|
174
|
+
send(ws, {
|
|
175
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
176
|
+
tabId,
|
|
177
|
+
data: { uploadId, error: 'Upload not found or expired' }
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
upload.stream.end(() => {
|
|
183
|
+
// Verify file was written
|
|
184
|
+
try {
|
|
185
|
+
const stat = statSync(upload.filePath);
|
|
186
|
+
const completed: CompletedUpload = {
|
|
187
|
+
uploadId,
|
|
188
|
+
fileName: upload.fileName,
|
|
189
|
+
filePath: upload.filePath,
|
|
190
|
+
isImage: upload.isImage,
|
|
191
|
+
mimeType: upload.mimeType,
|
|
192
|
+
fileSize: stat.size,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Store completed upload for this tab
|
|
196
|
+
const tabUploads = this.completedUploads.get(tabId) || [];
|
|
197
|
+
tabUploads.push(completed);
|
|
198
|
+
this.completedUploads.set(tabId, tabUploads);
|
|
199
|
+
|
|
200
|
+
this.activeUploads.delete(uploadId);
|
|
201
|
+
|
|
202
|
+
send(ws, {
|
|
203
|
+
type: 'fileUploadReady' as WebSocketResponse['type'],
|
|
204
|
+
tabId,
|
|
205
|
+
data: { uploadId, filePath: upload.filePath, fileName: upload.fileName }
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
send(ws, {
|
|
210
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
211
|
+
tabId,
|
|
212
|
+
data: { uploadId, error: `File verification failed: ${errorMsg}` }
|
|
213
|
+
});
|
|
214
|
+
this.activeUploads.delete(uploadId);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
handleUploadCancel(
|
|
220
|
+
_ws: WSContext,
|
|
221
|
+
_send: (ws: WSContext, response: WebSocketResponse) => void,
|
|
222
|
+
_tabId: string,
|
|
223
|
+
data: { uploadId: string }
|
|
224
|
+
): void {
|
|
225
|
+
this.cancelUpload(data.uploadId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private cancelUpload(uploadId: string): void {
|
|
229
|
+
const upload = this.activeUploads.get(uploadId);
|
|
230
|
+
if (!upload) return;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
upload.stream.destroy();
|
|
234
|
+
if (existsSync(upload.filePath)) {
|
|
235
|
+
rmSync(upload.filePath, { force: true });
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Ignore cleanup errors
|
|
239
|
+
}
|
|
240
|
+
this.activeUploads.delete(uploadId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private cleanupStaleUploads(): void {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
for (const [uploadId, upload] of this.activeUploads) {
|
|
246
|
+
if (now - upload.lastActivity > UPLOAD_TIMEOUT_MS) {
|
|
247
|
+
console.warn(`[FileUploadHandler] Upload ${uploadId} timed out, cleaning up`);
|
|
248
|
+
this.cancelUpload(uploadId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
destroy(): void {
|
|
254
|
+
clearInterval(this.cleanupInterval);
|
|
255
|
+
for (const uploadId of this.activeUploads.keys()) {
|
|
256
|
+
this.cancelUpload(uploadId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -188,13 +188,13 @@ const GIT_PR_TYPES = new Set([
|
|
|
188
188
|
|
|
189
189
|
// Worktree/merge message types that route to git-worktree-handlers
|
|
190
190
|
const GIT_WORKTREE_TYPES = new Set([
|
|
191
|
-
'gitWorktreeList', 'gitWorktreeCreate', 'gitWorktreeRemove',
|
|
191
|
+
'gitWorktreeList', 'gitWorktreeCreate', 'gitWorktreeCreateAndAssign', 'gitWorktreeRemove',
|
|
192
192
|
'tabWorktreeSwitch', 'gitWorktreePush', 'gitWorktreeCreatePR',
|
|
193
193
|
'gitMergePreview', 'gitWorktreeMerge', 'gitMergeAbort', 'gitMergeComplete',
|
|
194
194
|
]);
|
|
195
195
|
|
|
196
196
|
/** Route git messages to appropriate sub-handler */
|
|
197
|
-
export function handleGitMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
197
|
+
export async function handleGitMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
198
198
|
const gitDir = ctx.gitDirectories.get(tabId) || workingDir;
|
|
199
199
|
|
|
200
200
|
if (GIT_PR_TYPES.has(msg.type)) {
|
|
@@ -202,7 +202,7 @@ export function handleGitMessage(ctx: HandlerContext, ws: WSContext, msg: WebSoc
|
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
if (GIT_WORKTREE_TYPES.has(msg.type)) {
|
|
205
|
-
handleGitWorktreeMessage(ctx, ws, msg, tabId, gitDir, workingDir);
|
|
205
|
+
await handleGitWorktreeMessage(ctx, ws, msg, tabId, gitDir, workingDir);
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
|