mstro-app 0.4.50 → 0.4.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/server/services/websocket/file-upload-handler.d.ts +6 -2
  2. package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
  3. package/dist/server/services/websocket/file-upload-handler.js +127 -46
  4. package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
  5. package/dist/server/services/websocket/git-branch-handlers.d.ts +1 -1
  6. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  7. package/dist/server/services/websocket/git-branch-handlers.js +21 -1
  8. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  9. package/dist/server/services/websocket/git-handlers.js +1 -1
  10. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  11. package/dist/server/services/websocket/git-worktree-handlers.d.ts +2 -0
  12. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  13. package/dist/server/services/websocket/git-worktree-handlers.js +2 -2
  14. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  15. package/dist/server/services/websocket/handler.js +4 -4
  16. package/dist/server/services/websocket/handler.js.map +1 -1
  17. package/dist/server/services/websocket/plan-board-handlers.d.ts +1 -0
  18. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  19. package/dist/server/services/websocket/plan-board-handlers.js +34 -0
  20. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  21. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  22. package/dist/server/services/websocket/plan-handlers.js +2 -1
  23. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  24. package/dist/server/services/websocket/types.d.ts +1 -1
  25. package/dist/server/services/websocket/types.d.ts.map +1 -1
  26. package/dist/server/services/websocket/types.js +1 -1
  27. package/dist/server/services/websocket/types.js.map +1 -1
  28. package/package.json +1 -1
  29. package/server/services/websocket/file-upload-handler.ts +159 -51
  30. package/server/services/websocket/git-branch-handlers.ts +28 -1
  31. package/server/services/websocket/git-handlers.ts +1 -1
  32. package/server/services/websocket/git-worktree-handlers.ts +2 -2
  33. package/server/services/websocket/handler.ts +4 -4
  34. package/server/services/websocket/plan-board-handlers.ts +42 -0
  35. package/server/services/websocket/plan-handlers.ts +2 -1
  36. package/server/services/websocket/types.ts +1 -1
@@ -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
- * writes them to .mstro/tmp/attachments/{tabId}/, and sends progress acks back.
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(private workingDir: string) {
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
- 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
- });
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
- // Create attachment directory
83
- const attachDir = join(this.workingDir, '.mstro', 'tmp', 'attachments', tabId);
84
- if (!existsSync(attachDir)) {
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
- // 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++;
95
+ if (!resolved.ok) {
96
+ sendError(resolved.error);
97
+ return;
96
98
  }
97
99
 
98
- const filePath = join(attachDir, targetFileName);
99
- const stream = createWriteStream(filePath);
100
-
101
- const uploadState: UploadState = {
100
+ this.activeUploads.set(uploadId, {
102
101
  uploadId,
103
- fileName: targetFileName,
102
+ fileName: resolved.fileName,
104
103
  fileSize,
105
104
  mimeType,
106
105
  isImage,
107
106
  totalChunks,
108
107
  receivedChunks: 0,
109
- filePath,
110
- stream,
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
- 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);
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: { uploadId, filePath: upload.filePath, fileName: upload.fileName }
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
+ }
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
4
  import { executeGitCommand, sendGitError } from './git-utils.js';
5
+ import { findWorktreePathForBranch, handleTabWorktreeSwitch } from './git-worktree-handlers.js';
5
6
  import type { HandlerContext } from './handler-context.js';
6
7
  import type { GitBranchEntry, WebSocketMessage, WSContext } from './types.js';
7
8
 
@@ -40,7 +41,28 @@ export async function handleGitListBranches(ctx: HandlerContext, ws: WSContext,
40
41
  }
41
42
  }
42
43
 
43
- export async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
44
+ /**
45
+ * If `branch` is already checked out in a worktree other than `workingDir`, redirect the tab
46
+ * there via the worktree-switch flow and return true. Git refuses `checkout` in this case, so
47
+ * treating it as a view change is both correct and the only thing that works.
48
+ */
49
+ async function redirectToWorktreeIfBranchCheckedOut(
50
+ ctx: HandlerContext, ws: WSContext,
51
+ tabId: string, branch: string, workingDir: string, rootWorkingDir: string,
52
+ ): Promise<boolean> {
53
+ const wtListResult = await executeGitCommand(['worktree', 'list', '--porcelain'], workingDir);
54
+ if (wtListResult.exitCode !== 0) return false;
55
+ const existingWt = findWorktreePathForBranch(wtListResult.stdout, branch);
56
+ if (!existingWt || existingWt === workingDir) return false;
57
+ await handleTabWorktreeSwitch(
58
+ ctx, ws,
59
+ { type: 'tabWorktreeSwitch', tabId, data: { tabId, worktreePath: existingWt } },
60
+ tabId, rootWorkingDir,
61
+ );
62
+ return true;
63
+ }
64
+
65
+ export async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, rootWorkingDir: string): Promise<void> {
44
66
  try {
45
67
  const { branch, create, startPoint } = msg.data || {};
46
68
  if (!branch) {
@@ -48,6 +70,11 @@ export async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg:
48
70
  return;
49
71
  }
50
72
 
73
+ // Skip the worktree redirect for `create` — a name collision there is a real user error.
74
+ if (!create && await redirectToWorktreeIfBranchCheckedOut(ctx, ws, tabId, branch, workingDir, rootWorkingDir)) {
75
+ return;
76
+ }
77
+
51
78
  const statusResult = await executeGitCommand(['status', '--porcelain'], workingDir);
52
79
  if (statusResult.stdout.trim()) {
53
80
  ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit or stash changes before switching branches' } });
@@ -53,7 +53,7 @@ export async function handleGitMessage(ctx: HandlerContext, ws: WSContext, msg:
53
53
  gitDiscoverRepos: () => handleGitDiscoverRepos(ctx, ws, tabId, workingDir),
54
54
  gitSetDirectory: () => handleGitSetDirectory(ctx, ws, msg, tabId, workingDir),
55
55
  gitListBranches: () => handleGitListBranches(ctx, ws, tabId, gitDir),
56
- gitCheckout: () => handleGitCheckout(ctx, ws, msg, tabId, gitDir),
56
+ gitCheckout: () => handleGitCheckout(ctx, ws, msg, tabId, gitDir, workingDir),
57
57
  gitCreateBranch: () => handleGitCreateBranch(ctx, ws, msg, tabId, gitDir),
58
58
  gitDeleteBranch: () => handleGitDeleteBranch(ctx, ws, msg, tabId, gitDir),
59
59
  gitDiff: () => handleGitDiff(ctx, ws, msg, tabId, gitDir),
@@ -201,7 +201,7 @@ async function handleGitWorktreeRemove(ctx: HandlerContext, ws: WSContext, msg:
201
201
  }
202
202
  }
203
203
 
204
- async function handleTabWorktreeSwitch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
204
+ export async function handleTabWorktreeSwitch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
205
205
  try {
206
206
  const { tabId: targetTabId, worktreePath } = msg.data || {};
207
207
  const resolvedTabId = targetTabId || tabId;
@@ -445,7 +445,7 @@ async function cleanupAfterMerge(
445
445
  return { warnings, removedWorktreePath };
446
446
  }
447
447
 
448
- function findWorktreePathForBranch(porcelainOutput: string, branchName: string): string | null {
448
+ export function findWorktreePathForBranch(porcelainOutput: string, branchName: string): string | null {
449
449
  let currentWtPath = '';
450
450
  const fullRef = `refs/heads/${branchName}`;
451
451
  for (const line of porcelainOutput.split('\n')) {
@@ -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),
@@ -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