mstro-app 0.4.47 → 0.4.51
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/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +3 -0
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +1 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +7 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +3 -1
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +5 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +6 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +9 -7
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/settings.d.ts +17 -0
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +18 -2
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.d.ts +6 -2
- package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.js +127 -46
- package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
- package/dist/server/services/websocket/handler.js +4 -4
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +34 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +2 -1
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +2 -2
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +4 -4
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +4 -1
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +2 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +1 -1
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +1 -1
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +4 -0
- package/server/cli/headless/runner.ts +1 -0
- package/server/cli/headless/types.ts +7 -1
- package/server/cli/improvisation-retry.ts +3 -0
- package/server/cli/improvisation-session-manager.ts +5 -1
- package/server/cli/improvisation-types.ts +6 -0
- package/server/services/plan/composer.ts +10 -7
- package/server/services/settings.ts +29 -2
- package/server/services/websocket/file-upload-handler.ts +159 -51
- package/server/services/websocket/handler.ts +4 -4
- package/server/services/websocket/plan-board-handlers.ts +42 -0
- package/server/services/websocket/plan-handlers.ts +2 -1
- package/server/services/websocket/session-handlers.ts +2 -2
- package/server/services/websocket/session-initialization.ts +4 -4
- package/server/services/websocket/settings-handlers.ts +4 -1
- package/server/services/websocket/tab-handlers.ts +2 -2
- package/server/services/websocket/types.ts +1 -1
|
@@ -89,7 +89,7 @@ function prepareAttachmentPrompt(
|
|
|
89
89
|
(warning) => {
|
|
90
90
|
ctx.broadcastToAll({
|
|
91
91
|
type: 'planPromptProgress',
|
|
92
|
-
data: { message: warning },
|
|
92
|
+
data: { message: warning, boardId: effectiveBoardId ?? null },
|
|
93
93
|
});
|
|
94
94
|
},
|
|
95
95
|
);
|
|
@@ -287,10 +287,12 @@ User request: ${userPrompt}`;
|
|
|
287
287
|
const { prompt: finalPrompt, imageAttachments, sessionId: attachmentSessionId } =
|
|
288
288
|
prepareAttachmentPrompt(ctx, enrichedPrompt, attachments, workingDir, cc.effectiveBoardId);
|
|
289
289
|
|
|
290
|
+
const streamBoardId = cc.effectiveBoardId ?? null;
|
|
291
|
+
|
|
290
292
|
try {
|
|
291
293
|
ctx.broadcastToAll({
|
|
292
294
|
type: 'planPromptProgress',
|
|
293
|
-
data: { message: 'Starting project planning...' },
|
|
295
|
+
data: { message: 'Starting project planning...', boardId: streamBoardId },
|
|
294
296
|
});
|
|
295
297
|
|
|
296
298
|
const runner = new ResilientRunner({
|
|
@@ -305,7 +307,7 @@ User request: ${userPrompt}`;
|
|
|
305
307
|
outputCallback: (text: string) => {
|
|
306
308
|
ctx.send(ws, {
|
|
307
309
|
type: 'planPromptStreaming',
|
|
308
|
-
data: { token: text },
|
|
310
|
+
data: { token: text, boardId: streamBoardId },
|
|
309
311
|
});
|
|
310
312
|
},
|
|
311
313
|
toolUseCallback: (() => {
|
|
@@ -315,7 +317,7 @@ User request: ${userPrompt}`;
|
|
|
315
317
|
if (message) {
|
|
316
318
|
ctx.broadcastToAll({
|
|
317
319
|
type: 'planPromptProgress',
|
|
318
|
-
data: { message },
|
|
320
|
+
data: { message, boardId: streamBoardId },
|
|
319
321
|
});
|
|
320
322
|
}
|
|
321
323
|
};
|
|
@@ -326,14 +328,14 @@ User request: ${userPrompt}`;
|
|
|
326
328
|
|
|
327
329
|
ctx.broadcastToAll({
|
|
328
330
|
type: 'planPromptProgress',
|
|
329
|
-
data: { message: 'Claude is planning your project...' },
|
|
331
|
+
data: { message: 'Claude is planning your project...', boardId: streamBoardId },
|
|
330
332
|
});
|
|
331
333
|
|
|
332
334
|
const result = await runner.run();
|
|
333
335
|
|
|
334
336
|
ctx.broadcastToAll({
|
|
335
337
|
type: 'planPromptProgress',
|
|
336
|
-
data: { message: 'Finalizing project plan...' },
|
|
338
|
+
data: { message: 'Finalizing project plan...', boardId: streamBoardId },
|
|
337
339
|
});
|
|
338
340
|
|
|
339
341
|
ctx.send(ws, {
|
|
@@ -342,6 +344,7 @@ User request: ${userPrompt}`;
|
|
|
342
344
|
response: result.completed ? 'Prompt executed successfully.' : (result.error || 'Unknown error'),
|
|
343
345
|
success: result.completed,
|
|
344
346
|
error: result.error || null,
|
|
347
|
+
boardId: streamBoardId,
|
|
345
348
|
},
|
|
346
349
|
});
|
|
347
350
|
|
|
@@ -353,7 +356,7 @@ User request: ${userPrompt}`;
|
|
|
353
356
|
} catch (error) {
|
|
354
357
|
ctx.send(ws, {
|
|
355
358
|
type: 'planError',
|
|
356
|
-
data: { error: error instanceof Error ? error.message : String(error) },
|
|
359
|
+
data: { error: error instanceof Error ? error.message : String(error), boardId: streamBoardId },
|
|
357
360
|
});
|
|
358
361
|
} finally {
|
|
359
362
|
cleanupAttachments(workingDir, attachmentSessionId);
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Structure:
|
|
10
10
|
* {
|
|
11
|
-
* "model": "opus"
|
|
11
|
+
* "model": "opus",
|
|
12
|
+
* "effortLevel": "auto"
|
|
12
13
|
* }
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -26,12 +27,22 @@ export interface MstroSettings {
|
|
|
26
27
|
* - Any other string is passed as --model <value>
|
|
27
28
|
*/
|
|
28
29
|
model: string
|
|
30
|
+
/**
|
|
31
|
+
* Effort level for main execution, passed to Claude Code as --effort.
|
|
32
|
+
* - 'auto' means don't pass --effort (let Claude Code use its per-model default:
|
|
33
|
+
* xhigh on Opus 4.7, high on Sonnet 4.6).
|
|
34
|
+
* - Any other string is passed as --effort <value>. Claude Code silently falls
|
|
35
|
+
* back to the highest supported level when a model doesn't support the value
|
|
36
|
+
* (e.g. xhigh → high on Sonnet 4.6). Haiku ignores it entirely.
|
|
37
|
+
*/
|
|
38
|
+
effortLevel: string
|
|
29
39
|
/** Per-repo preferred PR base branch, keyed by normalized remote URL */
|
|
30
40
|
prBaseBranches?: Record<string, string>
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
const DEFAULT_SETTINGS: MstroSettings = {
|
|
34
|
-
model: 'opus'
|
|
44
|
+
model: 'opus',
|
|
45
|
+
effortLevel: 'auto'
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -90,6 +101,22 @@ export function setModel(model: string): void {
|
|
|
90
101
|
saveSettings(settings)
|
|
91
102
|
}
|
|
92
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Get the current effort level setting
|
|
106
|
+
*/
|
|
107
|
+
export function getEffortLevel(): string {
|
|
108
|
+
return getSettings().effortLevel
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update just the effort level setting
|
|
113
|
+
*/
|
|
114
|
+
export function setEffortLevel(effortLevel: string): void {
|
|
115
|
+
const settings = getSettings()
|
|
116
|
+
settings.effortLevel = effortLevel
|
|
117
|
+
saveSettings(settings)
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
/** Normalize a remote URL into a stable key (e.g. "github.com/owner/repo") */
|
|
94
121
|
function normalizeRemoteUrl(remoteUrl: string): string {
|
|
95
122
|
return remoteUrl
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Chunked File Upload Handler
|
|
6
6
|
*
|
|
7
|
-
* Receives files in chunks over WebSocket from remote web clients
|
|
8
|
-
*
|
|
7
|
+
* Receives files in chunks over WebSocket from remote web clients.
|
|
8
|
+
* - When `targetPath` is provided: streams to <workingDir>/<targetPath> (drag-drop into file tree).
|
|
9
|
+
* - Otherwise: streams to .mstro/tmp/attachments/{tabId}/ (prompt attachments).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { WriteStream } from 'node:fs';
|
|
12
13
|
import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { containsDangerousPatterns, validatePathWithinWorkingDir } from '../pathUtils.js';
|
|
16
|
+
import type { HandlerContext } from './handler-context.js';
|
|
14
17
|
import type { WebSocketResponse, WSContext } from './types.js';
|
|
15
18
|
|
|
16
19
|
interface UploadState {
|
|
@@ -22,6 +25,8 @@ interface UploadState {
|
|
|
22
25
|
totalChunks: number;
|
|
23
26
|
receivedChunks: number;
|
|
24
27
|
filePath: string;
|
|
28
|
+
/** Path relative to workingDir (only set when destination is the file tree, not the attachments dir) */
|
|
29
|
+
targetRelPath?: string;
|
|
25
30
|
stream: WriteStream;
|
|
26
31
|
lastActivity: number;
|
|
27
32
|
}
|
|
@@ -44,7 +49,10 @@ export class FileUploadHandler {
|
|
|
44
49
|
private completedUploads = new Map<string, CompletedUpload[]>(); // tabId -> completed uploads
|
|
45
50
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
46
51
|
|
|
47
|
-
constructor(
|
|
52
|
+
constructor(
|
|
53
|
+
private workingDir: string,
|
|
54
|
+
private ctx?: HandlerContext,
|
|
55
|
+
) {
|
|
48
56
|
// Periodically clean up stale uploads
|
|
49
57
|
this.cleanupInterval = setInterval(() => this.cleanupStaleUploads(), 30_000);
|
|
50
58
|
}
|
|
@@ -65,59 +73,48 @@ export class FileUploadHandler {
|
|
|
65
73
|
ws: WSContext,
|
|
66
74
|
send: (ws: WSContext, response: WebSocketResponse) => void,
|
|
67
75
|
tabId: string,
|
|
68
|
-
data: { uploadId: string; fileName: string; fileSize: number; mimeType: string; isImage: boolean; totalChunks: number }
|
|
76
|
+
data: { uploadId: string; fileName: string; fileSize: number; mimeType: string; isImage: boolean; totalChunks: number; targetPath?: string; overwrite?: boolean },
|
|
77
|
+
permission?: 'view',
|
|
69
78
|
): void {
|
|
70
|
-
const { uploadId, fileName, fileSize, mimeType, isImage, totalChunks } = data;
|
|
79
|
+
const { uploadId, fileName, fileSize, mimeType, isImage, totalChunks, targetPath, overwrite } = data;
|
|
80
|
+
const sendError = (error: string) => send(ws, {
|
|
81
|
+
type: 'fileUploadError' as WebSocketResponse['type'],
|
|
82
|
+
tabId,
|
|
83
|
+
data: { uploadId, error },
|
|
84
|
+
});
|
|
71
85
|
|
|
72
|
-
// Validate file size
|
|
73
86
|
if (fileSize > MAX_FILE_SIZE) {
|
|
74
|
-
|
|
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
|
-
});
|
|
87
|
+
sendError(`File too large: ${(fileSize / 1024 / 1024).toFixed(1)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit`);
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
mkdirSync(attachDir, { recursive: true });
|
|
86
|
-
}
|
|
91
|
+
const resolved = targetPath !== undefined
|
|
92
|
+
? resolveFileTreeTarget({ targetPath, fileName, workingDir: this.workingDir, permission, overwrite })
|
|
93
|
+
: resolveAttachmentTarget({ workingDir: this.workingDir, tabId, fileName });
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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++;
|
|
95
|
+
if (!resolved.ok) {
|
|
96
|
+
sendError(resolved.error);
|
|
97
|
+
return;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
const stream = createWriteStream(filePath);
|
|
100
|
-
|
|
101
|
-
const uploadState: UploadState = {
|
|
100
|
+
this.activeUploads.set(uploadId, {
|
|
102
101
|
uploadId,
|
|
103
|
-
fileName:
|
|
102
|
+
fileName: resolved.fileName,
|
|
104
103
|
fileSize,
|
|
105
104
|
mimeType,
|
|
106
105
|
isImage,
|
|
107
106
|
totalChunks,
|
|
108
107
|
receivedChunks: 0,
|
|
109
|
-
filePath,
|
|
110
|
-
|
|
108
|
+
filePath: resolved.filePath,
|
|
109
|
+
targetRelPath: resolved.targetRelPath,
|
|
110
|
+
stream: createWriteStream(resolved.filePath),
|
|
111
111
|
lastActivity: Date.now(),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
this.activeUploads.set(uploadId, uploadState);
|
|
112
|
+
});
|
|
115
113
|
|
|
116
|
-
// Send ack for start
|
|
117
114
|
send(ws, {
|
|
118
115
|
type: 'fileUploadAck' as WebSocketResponse['type'],
|
|
119
116
|
tabId,
|
|
120
|
-
data: { uploadId, chunkIndex: -1, status: 'ok' }
|
|
117
|
+
data: { uploadId, chunkIndex: -1, status: 'ok' },
|
|
121
118
|
});
|
|
122
119
|
}
|
|
123
120
|
|
|
@@ -183,27 +180,50 @@ export class FileUploadHandler {
|
|
|
183
180
|
// Verify file was written
|
|
184
181
|
try {
|
|
185
182
|
const stat = statSync(upload.filePath);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
|
|
184
|
+
// Only stash as a pending prompt attachment for the attachment-path case.
|
|
185
|
+
// File-tree drops land at the final destination directly.
|
|
186
|
+
if (upload.targetRelPath === undefined) {
|
|
187
|
+
const completed: CompletedUpload = {
|
|
188
|
+
uploadId,
|
|
189
|
+
fileName: upload.fileName,
|
|
190
|
+
filePath: upload.filePath,
|
|
191
|
+
isImage: upload.isImage,
|
|
192
|
+
mimeType: upload.mimeType,
|
|
193
|
+
fileSize: stat.size,
|
|
194
|
+
};
|
|
195
|
+
const tabUploads = this.completedUploads.get(tabId) || [];
|
|
196
|
+
tabUploads.push(completed);
|
|
197
|
+
this.completedUploads.set(tabId, tabUploads);
|
|
198
|
+
}
|
|
199
199
|
|
|
200
200
|
this.activeUploads.delete(uploadId);
|
|
201
201
|
|
|
202
202
|
send(ws, {
|
|
203
203
|
type: 'fileUploadReady' as WebSocketResponse['type'],
|
|
204
204
|
tabId,
|
|
205
|
-
data: {
|
|
205
|
+
data: {
|
|
206
|
+
uploadId,
|
|
207
|
+
filePath: upload.filePath,
|
|
208
|
+
fileName: upload.fileName,
|
|
209
|
+
...(upload.targetRelPath !== undefined ? { targetPath: upload.targetRelPath } : {}),
|
|
210
|
+
},
|
|
206
211
|
});
|
|
212
|
+
|
|
213
|
+
// Broadcast fileCreated so all connected clients (including the uploader)
|
|
214
|
+
// refresh their file tree. The final path may differ from the requested
|
|
215
|
+
// targetPath due to counter-based disambiguation, so we broadcast the resolved path.
|
|
216
|
+
if (upload.targetRelPath !== undefined && this.ctx) {
|
|
217
|
+
this.ctx.broadcastToAll({
|
|
218
|
+
type: 'fileCreated' as WebSocketResponse['type'],
|
|
219
|
+
data: {
|
|
220
|
+
path: upload.targetRelPath,
|
|
221
|
+
name: upload.fileName,
|
|
222
|
+
size: stat.size,
|
|
223
|
+
modifiedAt: new Date().toISOString(),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
207
227
|
} catch (err) {
|
|
208
228
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
209
229
|
send(ws, {
|
|
@@ -257,3 +277,91 @@ export class FileUploadHandler {
|
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
}
|
|
280
|
+
|
|
281
|
+
type ResolveResult =
|
|
282
|
+
| { ok: true; filePath: string; fileName: string; targetRelPath?: string }
|
|
283
|
+
| { ok: false; error: string };
|
|
284
|
+
|
|
285
|
+
function splitExt(fileName: string): { base: string; ext: string } {
|
|
286
|
+
const dot = fileName.lastIndexOf('.');
|
|
287
|
+
if (dot <= 0) return { base: fileName, ext: '' };
|
|
288
|
+
return { base: fileName.slice(0, dot), ext: fileName.slice(dot) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ensureDir(dir: string): void {
|
|
292
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resolveFileTreeTarget(opts: {
|
|
296
|
+
targetPath: string;
|
|
297
|
+
fileName: string;
|
|
298
|
+
workingDir: string;
|
|
299
|
+
permission?: 'view';
|
|
300
|
+
overwrite?: boolean;
|
|
301
|
+
}): ResolveResult {
|
|
302
|
+
const { targetPath, fileName, workingDir, permission, overwrite } = opts;
|
|
303
|
+
|
|
304
|
+
if (permission === 'view') {
|
|
305
|
+
return { ok: false, error: 'View-only sessions cannot upload files' };
|
|
306
|
+
}
|
|
307
|
+
if (containsDangerousPatterns(targetPath) || containsDangerousPatterns(fileName)) {
|
|
308
|
+
return { ok: false, error: 'Invalid characters in target path' };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const validation = validatePathWithinWorkingDir(targetPath, workingDir);
|
|
312
|
+
if (!validation.valid) {
|
|
313
|
+
return { ok: false, error: validation.error ?? 'Invalid target path' };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const resolvedPath = validation.resolvedPath;
|
|
317
|
+
const parent = dirname(resolvedPath);
|
|
318
|
+
try {
|
|
319
|
+
ensureDir(parent);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
return { ok: false, error: `Failed to create parent directory: ${msg}` };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const lastSlash = targetPath.lastIndexOf('/');
|
|
326
|
+
const relDir = lastSlash >= 0 ? targetPath.slice(0, lastSlash) : '';
|
|
327
|
+
|
|
328
|
+
let finalName = fileName;
|
|
329
|
+
let finalAbs = resolvedPath;
|
|
330
|
+
if (existsSync(finalAbs) && !overwrite) {
|
|
331
|
+
const { base, ext } = splitExt(fileName);
|
|
332
|
+
let counter = 1;
|
|
333
|
+
do {
|
|
334
|
+
finalName = `${base} (${counter})${ext}`;
|
|
335
|
+
finalAbs = join(parent, finalName);
|
|
336
|
+
counter++;
|
|
337
|
+
} while (existsSync(finalAbs));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const finalRel = relDir ? `${relDir}/${finalName}` : finalName;
|
|
341
|
+
return { ok: true, filePath: finalAbs, fileName: finalName, targetRelPath: finalRel };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveAttachmentTarget(opts: {
|
|
345
|
+
workingDir: string;
|
|
346
|
+
tabId: string;
|
|
347
|
+
fileName: string;
|
|
348
|
+
}): ResolveResult {
|
|
349
|
+
const { workingDir, tabId, fileName } = opts;
|
|
350
|
+
const attachDir = join(workingDir, '.mstro', 'tmp', 'attachments', tabId);
|
|
351
|
+
try {
|
|
352
|
+
ensureDir(attachDir);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
355
|
+
return { ok: false, error: `Failed to create attachment directory: ${msg}` };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let finalName = fileName;
|
|
359
|
+
let counter = 1;
|
|
360
|
+
while (existsSync(join(attachDir, finalName))) {
|
|
361
|
+
const { base, ext } = splitExt(fileName);
|
|
362
|
+
finalName = `${base}-${counter}${ext}`;
|
|
363
|
+
counter++;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { ok: true, filePath: join(attachDir, finalName), fileName: finalName };
|
|
367
|
+
}
|
|
@@ -220,20 +220,20 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
220
220
|
case 'git': return handleGitMessage(this, ws, msg, tabId, workingDir);
|
|
221
221
|
case 'quality': return handleQualityMessage(this, ws, msg, tabId, workingDir, permission);
|
|
222
222
|
case 'plan': return handlePlanMessage(this, ws, msg, tabId, workingDir, permission);
|
|
223
|
-
case 'fileUpload': return this.handleFileUploadMessage(ws, msg, tabId, workingDir);
|
|
223
|
+
case 'fileUpload': return this.handleFileUploadMessage(ws, msg, tabId, workingDir, permission);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
private handleFileUploadMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
227
|
+
private handleFileUploadMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): void {
|
|
228
228
|
if (!this.fileUploadHandler) {
|
|
229
|
-
this.fileUploadHandler = new FileUploadHandler(workingDir);
|
|
229
|
+
this.fileUploadHandler = new FileUploadHandler(workingDir, this);
|
|
230
230
|
}
|
|
231
231
|
const handler = this.fileUploadHandler;
|
|
232
232
|
const send = this.send.bind(this);
|
|
233
233
|
|
|
234
234
|
switch (msg.type) {
|
|
235
235
|
case 'fileUploadStart':
|
|
236
|
-
handler.handleUploadStart(ws, send, tabId, msg.data);
|
|
236
|
+
handler.handleUploadStart(ws, send, tabId, msg.data, permission);
|
|
237
237
|
break;
|
|
238
238
|
case 'fileUploadChunk':
|
|
239
239
|
handler.handleUploadChunk(ws, send, tabId, msg.data);
|
|
@@ -209,6 +209,48 @@ export function handleArchiveBoard(
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
export function handleRestoreBoard(
|
|
213
|
+
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
214
|
+
workingDir: string, permission?: 'view',
|
|
215
|
+
): void {
|
|
216
|
+
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
217
|
+
|
|
218
|
+
const boardId = msg.data?.boardId;
|
|
219
|
+
if (!boardId) {
|
|
220
|
+
ctx.send(ws, { type: 'planError', data: { error: 'Board ID required' } });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const pmDir = resolvePmDir(workingDir);
|
|
225
|
+
if (!pmDir) return;
|
|
226
|
+
|
|
227
|
+
const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
|
|
228
|
+
if (!existsSync(boardMdPath)) {
|
|
229
|
+
ctx.send(ws, { type: 'planError', data: { error: `Board not found: ${boardId}` } });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let content = readFileSync(boardMdPath, 'utf-8');
|
|
234
|
+
content = replaceFrontMatterField(content, 'status', 'active');
|
|
235
|
+
writeFileSync(boardMdPath, content, 'utf-8');
|
|
236
|
+
|
|
237
|
+
const workspacePath = join(pmDir, 'workspace.json');
|
|
238
|
+
if (existsSync(workspacePath)) {
|
|
239
|
+
const workspace: Workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
|
|
240
|
+
if (!workspace.boardOrder.includes(boardId)) {
|
|
241
|
+
workspace.boardOrder.push(boardId);
|
|
242
|
+
}
|
|
243
|
+
workspace.activeBoardId = boardId;
|
|
244
|
+
writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
|
|
245
|
+
ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
249
|
+
if (boardState) {
|
|
250
|
+
ctx.broadcastToAll({ type: 'planBoardUpdated', data: boardState.board });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
212
254
|
export function handleGetBoard(
|
|
213
255
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
214
256
|
workingDir: string,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { HandlerContext } from './handler-context.js';
|
|
12
|
-
import { handleArchiveBoard, handleChatToBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
12
|
+
import { handleArchiveBoard, handleChatToBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleRestoreBoard, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
13
13
|
import { handleExecute, handleExecuteEpic, handlePause, handlePrompt, handleResume, handleStop } from './plan-execution-handlers.js';
|
|
14
14
|
import { handleCreateIssue, handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
|
|
15
15
|
import { handleActivateSprint, handleCompleteSprint, handleCreateSprint, handleGetSprintArtifacts } from './plan-sprint-handlers.js';
|
|
@@ -47,6 +47,7 @@ export function handlePlanMessage(
|
|
|
47
47
|
planCreateBoard: () => handleCreateBoard(ctx, ws, msg, workingDir, permission),
|
|
48
48
|
planUpdateBoard: () => handleUpdateBoard(ctx, ws, msg, workingDir, permission),
|
|
49
49
|
planArchiveBoard: () => handleArchiveBoard(ctx, ws, msg, workingDir, permission),
|
|
50
|
+
planRestoreBoard: () => handleRestoreBoard(ctx, ws, msg, workingDir, permission),
|
|
50
51
|
planGetBoard: () => handleGetBoard(ctx, ws, msg, workingDir),
|
|
51
52
|
planGetBoardState: () => handleGetBoardState(ctx, ws, msg, workingDir),
|
|
52
53
|
planReorderBoards: () => handleReorderBoards(ctx, ws, msg, workingDir, permission),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type { FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
5
|
-
import { getModel } from '../settings.js';
|
|
5
|
+
import { getEffortLevel, getModel } from '../settings.js';
|
|
6
6
|
import type { HandlerContext } from './handler-context.js';
|
|
7
7
|
import { runQualityScan } from './quality-service.js';
|
|
8
8
|
import { resolveSkillPrompt } from './skill-handlers.js';
|
|
@@ -240,7 +240,7 @@ export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: We
|
|
|
240
240
|
case 'new': {
|
|
241
241
|
const oldSession = requireSession(ctx, ws, tabId);
|
|
242
242
|
const oldSessionId = oldSession.getSessionInfo().sessionId;
|
|
243
|
-
const newSession = oldSession.startNewSession({ model: getModel() });
|
|
243
|
+
const newSession = oldSession.startNewSession({ model: getModel(), effortLevel: getEffortLevel() });
|
|
244
244
|
oldSession.destroy();
|
|
245
245
|
ctx.sessions.delete(oldSessionId);
|
|
246
246
|
setupSessionListeners(ctx, newSession, ws, tabId);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
5
|
-
import { getModel } from '../settings.js';
|
|
5
|
+
import { getEffortLevel, getModel } from '../settings.js';
|
|
6
6
|
import type { HandlerContext } from './handler-context.js';
|
|
7
7
|
import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
|
|
8
8
|
import type { SessionRegistry } from './session-registry.js';
|
|
@@ -78,7 +78,7 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// 3. Create new session
|
|
81
|
-
const session = new ImprovisationSessionManager({ workingDir, model: getModel() });
|
|
81
|
+
const session = new ImprovisationSessionManager({ workingDir, model: getModel(), effortLevel: getEffortLevel() });
|
|
82
82
|
setupSessionListeners(ctx, session, ws, tabId);
|
|
83
83
|
|
|
84
84
|
const sessionId = session.getSessionInfo().sessionId;
|
|
@@ -134,10 +134,10 @@ export async function resumeHistoricalSession(
|
|
|
134
134
|
let isNewSession = false;
|
|
135
135
|
|
|
136
136
|
try {
|
|
137
|
-
session = ImprovisationSessionManager.resumeFromHistory(workingDir, historicalSessionId, { model: getModel() });
|
|
137
|
+
session = ImprovisationSessionManager.resumeFromHistory(workingDir, historicalSessionId, { model: getModel(), effortLevel: getEffortLevel() });
|
|
138
138
|
} catch (error: unknown) {
|
|
139
139
|
console.warn(`[WebSocketImproviseHandler] Could not resume session ${historicalSessionId}: ${error instanceof Error ? error.message : String(error)}. Creating new session.`);
|
|
140
|
-
session = new ImprovisationSessionManager({ workingDir, model: getModel() });
|
|
140
|
+
session = new ImprovisationSessionManager({ workingDir, model: getModel(), effortLevel: getEffortLevel() });
|
|
141
141
|
isNewSession = true;
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { getSettings, setModel } from '../settings.js';
|
|
7
|
+
import { getSettings, setEffortLevel, setModel } from '../settings.js';
|
|
8
8
|
import type { HandlerContext } from './handler-context.js';
|
|
9
9
|
import type { WebSocketMessage, WSContext } from './types.js';
|
|
10
10
|
|
|
@@ -16,6 +16,9 @@ export function handleUpdateSettings(ctx: HandlerContext, _ws: WSContext, msg: W
|
|
|
16
16
|
if (msg.data?.model !== undefined) {
|
|
17
17
|
setModel(msg.data.model);
|
|
18
18
|
}
|
|
19
|
+
if (msg.data?.effortLevel !== undefined) {
|
|
20
|
+
setEffortLevel(msg.data.effortLevel);
|
|
21
|
+
}
|
|
19
22
|
ctx.broadcastToAll({ type: 'settingsUpdated', data: getSettings() });
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
|
|
5
|
-
import { getModel } from '../settings.js';
|
|
5
|
+
import { getEffortLevel, getModel } from '../settings.js';
|
|
6
6
|
import type { HandlerContext } from './handler-context.js';
|
|
7
7
|
import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
|
|
8
8
|
import type { WebSocketMessage, WSContext } from './types.js';
|
|
@@ -130,7 +130,7 @@ export async function handleCreateTab(ctx: HandlerContext, ws: WSContext, workin
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const session = new ImprovisationSessionManager({ workingDir, model: getModel() });
|
|
133
|
+
const session = new ImprovisationSessionManager({ workingDir, model: getModel(), effortLevel: getEffortLevel() });
|
|
134
134
|
setupSessionListeners(ctx, session, ws, tabId);
|
|
135
135
|
|
|
136
136
|
const sessionId = session.getSessionInfo().sessionId;
|
|
@@ -48,7 +48,7 @@ const FileUploadMessages = ['fileUploadStart', 'fileUploadChunk', 'fileUploadCom
|
|
|
48
48
|
|
|
49
49
|
const PlanMessages = ['planInit', 'planGetState', 'planListIssues', 'planGetIssue', 'planGetSprint', 'planGetMilestone', 'planCreateIssue', 'planUpdateIssue', 'planDeleteIssue', 'planScaffold', 'planPrompt', 'planExecute', 'planExecuteEpic', 'planPause', 'planStop', 'planResume'] as const;
|
|
50
50
|
|
|
51
|
-
const PlanBoardMessages = ['planCreateBoard', 'planUpdateBoard', 'planArchiveBoard', 'planGetBoard', 'planGetBoardState', 'planReorderBoards', 'planSetActiveBoard', 'planGetBoardArtifacts'] as const;
|
|
51
|
+
const PlanBoardMessages = ['planCreateBoard', 'planUpdateBoard', 'planArchiveBoard', 'planRestoreBoard', 'planGetBoard', 'planGetBoardState', 'planReorderBoards', 'planSetActiveBoard', 'planGetBoardArtifacts'] as const;
|
|
52
52
|
|
|
53
53
|
const PlanSprintMessages = ['planCreateSprint', 'planActivateSprint', 'planCompleteSprint', 'planGetSprintArtifacts'] as const;
|
|
54
54
|
|