@myrialabs/clopen 0.1.2 → 0.1.4

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 (63) hide show
  1. package/CONTRIBUTING.md +40 -355
  2. package/README.md +46 -113
  3. package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
  4. package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
  5. package/backend/lib/mcp/config.ts +7 -3
  6. package/backend/lib/mcp/servers/helper.ts +25 -14
  7. package/backend/lib/mcp/servers/index.ts +7 -2
  8. package/backend/lib/project/status-manager.ts +221 -181
  9. package/frontend/lib/components/chat/ChatInterface.svelte +7 -0
  10. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +16 -9
  11. package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
  12. package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
  13. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
  14. package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
  15. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
  16. package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
  17. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
  18. package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
  19. package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
  20. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
  21. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
  22. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
  23. package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
  24. package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
  25. package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
  26. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
  27. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
  28. package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
  29. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
  30. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
  31. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
  32. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
  33. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
  34. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
  35. package/frontend/lib/components/common/Button.svelte +1 -1
  36. package/frontend/lib/components/common/Card.svelte +3 -3
  37. package/frontend/lib/components/common/Input.svelte +3 -3
  38. package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
  39. package/frontend/lib/components/common/Select.svelte +6 -6
  40. package/frontend/lib/components/common/Textarea.svelte +3 -3
  41. package/frontend/lib/components/files/FileViewer.svelte +1 -1
  42. package/frontend/lib/components/git/ChangesSection.svelte +2 -4
  43. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
  44. package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
  45. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
  46. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
  47. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
  48. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
  49. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
  50. package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
  51. package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
  52. package/frontend/lib/components/workspace/PanelHeader.svelte +623 -505
  53. package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
  54. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
  55. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  56. package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
  57. package/frontend/lib/services/project/status.service.ts +160 -159
  58. package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
  59. package/package.json +1 -1
  60. package/scripts/pre-publish-check.sh +0 -142
  61. package/scripts/setup-hooks.sh +0 -134
  62. package/scripts/validate-branch-name.sh +0 -47
  63. package/scripts/validate-commit-msg.sh +0 -42
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { debug } from '$shared/utils/logger';
7
+ import type { IconName } from '$shared/types/ui/icons';
7
8
 
8
9
  // ============================================
9
10
  // TYPE DEFINITIONS
@@ -146,146 +147,181 @@ export function updateSplitRatio(node: SplitNode, path: number[], newRatio: numb
146
147
  return { ...node, children: newChildren };
147
148
  }
148
149
 
150
+ /**
151
+ * Swap two panels' positions in the tree
152
+ */
153
+ function swapPanelsInTree(node: SplitNode, panelA: PanelId, panelB: PanelId): SplitNode {
154
+ if (node.type === 'panel') {
155
+ if (node.panelId === panelA) return createPanel(panelB);
156
+ if (node.panelId === panelB) return createPanel(panelA);
157
+ return node;
158
+ }
159
+
160
+ return {
161
+ ...node,
162
+ children: [
163
+ swapPanelsInTree(node.children[0], panelA, panelB),
164
+ swapPanelsInTree(node.children[1], panelA, panelB)
165
+ ] as [SplitNode, SplitNode]
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Split a panel leaf into a split container with the original panel + new panel
171
+ */
172
+ function splitPanelInTree(
173
+ node: SplitNode,
174
+ targetPanelId: PanelId,
175
+ direction: SplitDirection,
176
+ newPanelId: PanelId | null
177
+ ): SplitNode {
178
+ if (node.type === 'panel') {
179
+ if (node.panelId === targetPanelId) {
180
+ return createSplit(direction, 50, createPanel(targetPanelId), createPanel(newPanelId));
181
+ }
182
+ return node;
183
+ }
184
+
185
+ return {
186
+ ...node,
187
+ children: [
188
+ splitPanelInTree(node.children[0], targetPanelId, direction, newPanelId),
189
+ splitPanelInTree(node.children[1], targetPanelId, direction, newPanelId)
190
+ ] as [SplitNode, SplitNode]
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Remove a panel from the tree and collapse its parent split.
196
+ * Returns null if the node itself is the target panel (root-level).
197
+ */
198
+ function removePanelFromTree(node: SplitNode, panelId: PanelId): SplitNode | null {
199
+ if (node.type === 'panel') {
200
+ return node.panelId === panelId ? null : node;
201
+ }
202
+
203
+ const [child1, child2] = node.children;
204
+
205
+ const newChild1 = removePanelFromTree(child1, panelId);
206
+ if (newChild1 === null) return child2;
207
+
208
+ const newChild2 = removePanelFromTree(child2, panelId);
209
+ if (newChild2 === null) return child1;
210
+
211
+ if (newChild1 !== child1 || newChild2 !== child2) {
212
+ return { ...node, children: [newChild1, newChild2] as [SplitNode, SplitNode] };
213
+ }
214
+
215
+ return node;
216
+ }
217
+
218
+ /**
219
+ * Replace a node at a specific path in the tree
220
+ */
221
+ function setNodeAtPath(root: SplitNode, path: number[], replacement: SplitNode): SplitNode {
222
+ if (path.length === 0) return replacement;
223
+ if (root.type === 'panel') return root;
224
+
225
+ const [nextIndex, ...restPath] = path;
226
+ const newChildren = [...root.children] as [SplitNode, SplitNode];
227
+ newChildren[nextIndex] = setNodeAtPath(newChildren[nextIndex], restPath, replacement);
228
+ return { ...root, children: newChildren };
229
+ }
230
+
231
+ /**
232
+ * Remove all empty (null) leaf nodes and collapse their parent splits
233
+ */
234
+ function cleanupEmptyNodes(node: SplitNode): SplitNode | null {
235
+ if (node.type === 'panel') {
236
+ return node.panelId ? node : null;
237
+ }
238
+
239
+ const child1 = cleanupEmptyNodes(node.children[0]);
240
+ const child2 = cleanupEmptyNodes(node.children[1]);
241
+
242
+ if (!child1 && !child2) return null;
243
+ if (!child1) return child2;
244
+ if (!child2) return child1;
245
+
246
+ if (child1 !== node.children[0] || child2 !== node.children[1]) {
247
+ return { ...node, children: [child1, child2] as [SplitNode, SplitNode] };
248
+ }
249
+
250
+ return node;
251
+ }
252
+
253
+ /**
254
+ * Get a node at a specific path in the tree
255
+ */
256
+ function getNodeAtPath(root: SplitNode, path: number[]): SplitNode | null {
257
+ if (path.length === 0) return root;
258
+ if (root.type === 'panel') return null;
259
+
260
+ const [nextIndex, ...restPath] = path;
261
+ return getNodeAtPath(root.children[nextIndex], restPath);
262
+ }
263
+
264
+ /**
265
+ * Count total leaf nodes in the tree (including empty slots)
266
+ */
267
+ function countLeafNodes(node: SplitNode): number {
268
+ if (node.type === 'panel') return 1;
269
+ return countLeafNodes(node.children[0]) + countLeafNodes(node.children[1]);
270
+ }
271
+
149
272
  // ============================================
150
273
  // BUILT-IN PRESETS
151
274
  // ============================================
152
275
 
153
276
  export const builtInPresets: LayoutPreset[] = [
154
277
  // ============================================
155
- // A. SINGLE PANEL FOCUS (5 presets)
278
+ // A. SINGLE PANEL (1 preset)
156
279
  // ============================================
157
280
  {
158
- id: 'focus-chat',
159
- name: 'Focus Chat',
160
- description: 'Chat only, full screen',
161
- icon: 'lucide:message-square',
281
+ id: 'focus',
282
+ name: 'Focus',
283
+ description: 'Single full panel',
284
+ icon: 'lucide:maximize-2',
162
285
  layout: createPanel('chat'),
163
286
  isCustom: false
164
287
  },
165
- {
166
- id: 'focus-files',
167
- name: 'Focus Files',
168
- description: 'File explorer fullscreen',
169
- icon: 'lucide:folder-open',
170
- layout: createPanel('files'),
171
- isCustom: false
172
- },
173
- {
174
- id: 'focus-preview',
175
- name: 'Focus Preview',
176
- description: 'Preview fullscreen',
177
- icon: 'lucide:monitor',
178
- layout: createPanel('preview'),
179
- isCustom: false
180
- },
181
- {
182
- id: 'focus-terminal',
183
- name: 'Focus Terminal',
184
- description: 'Terminal fullscreen',
185
- icon: 'lucide:terminal-square',
186
- layout: createPanel('terminal'),
187
- isCustom: false
188
- },
189
- {
190
- id: 'focus-git',
191
- name: 'Focus Git',
192
- description: 'Source control fullscreen',
193
- icon: 'lucide:git-branch',
194
- layout: createPanel('git'),
195
- isCustom: false
196
- },
197
288
 
198
289
  // ============================================
199
- // B. TWO PANEL LAYOUTS (6 presets)
290
+ // B. TWO PANELS (3 presets)
200
291
  // ============================================
201
292
  {
202
- id: 'chat-files',
203
- name: 'Chat + Files',
204
- description: '50/50 vertical split',
205
- icon: 'lucide:layout-panel-left',
293
+ id: 'side-by-side',
294
+ name: 'Dual Columns',
295
+ description: 'Two equal columns',
296
+ icon: 'lucide:columns-2',
206
297
  layout: createSplit('vertical', 50, createPanel('chat'), createPanel('files')),
207
298
  isCustom: false
208
299
  },
209
300
  {
210
- id: 'chat-preview',
211
- name: 'Chat + Preview',
212
- description: 'Live preview development',
213
- icon: 'lucide:app-window',
214
- layout: createSplit('vertical', 50, createPanel('chat'), createPanel('preview')),
215
- isCustom: false
216
- },
217
- {
218
- id: 'chat-terminal',
219
- name: 'Chat + Terminal',
220
- description: 'Backend/CLI development',
221
- icon: 'lucide:square-terminal',
222
- layout: createSplit('vertical', 50, createPanel('chat'), createPanel('terminal')),
301
+ id: 'top-bottom',
302
+ name: 'Dual Rows',
303
+ description: 'Two equal rows',
304
+ icon: 'lucide:rows-2',
305
+ layout: createSplit('horizontal', 50, createPanel('files'), createPanel('terminal')),
223
306
  isCustom: false
224
307
  },
225
308
  {
226
- id: 'files-preview',
227
- name: 'Files + Preview',
228
- description: 'Manual coding workflow',
309
+ id: 'sidebar-main',
310
+ name: 'Sidebar',
311
+ description: 'Narrow left, wide right',
229
312
  icon: 'lucide:panel-left',
230
- layout: createSplit('vertical', 50, createPanel('files'), createPanel('preview')),
231
- isCustom: false
232
- },
233
- {
234
- id: 'files-terminal',
235
- name: 'Files + Terminal',
236
- description: 'File management + execution',
237
- icon: 'lucide:folder-tree',
238
- layout: createSplit('vertical', 50, createPanel('files'), createPanel('terminal')),
239
- isCustom: false
240
- },
241
- {
242
- id: 'preview-terminal',
243
- name: 'Preview + Terminal',
244
- description: 'Frontend testing',
245
- icon: 'lucide:layout-panel-top',
246
- layout: createSplit('horizontal', 50, createPanel('preview'), createPanel('terminal')),
247
- isCustom: false
248
- },
249
- {
250
- id: 'chat-git',
251
- name: 'Chat + Git',
252
- description: 'AI-assisted source control',
253
- icon: 'lucide:git-merge',
254
- layout: createSplit('vertical', 50, createPanel('chat'), createPanel('git')),
255
- isCustom: false
256
- },
257
- {
258
- id: 'files-git',
259
- name: 'Files + Git',
260
- description: 'File editing + source control',
261
- icon: 'lucide:git-compare',
262
- layout: createSplit('vertical', 50, createPanel('files'), createPanel('git')),
313
+ layout: createSplit('vertical', 25, createPanel('files'), createPanel('chat')),
263
314
  isCustom: false
264
315
  },
265
316
 
266
317
  // ============================================
267
- // C. THREE PANEL LAYOUTS (8 presets)
318
+ // C. THREE PANELS (4 presets)
268
319
  // ============================================
269
320
  {
270
- id: 'code-review',
271
- name: 'Code Review',
272
- description: 'Chat + Files + Git',
273
- icon: 'lucide:git-pull-request',
274
- // Layout: [Chat (33%) | Files/Git (67%)]
275
- layout: createSplit(
276
- 'vertical',
277
- 33,
278
- createPanel('chat'),
279
- createSplit('horizontal', 50, createPanel('files'), createPanel('git'))
280
- ),
281
- isCustom: false
282
- },
283
- {
284
- id: 'frontend-dev',
285
- name: 'Frontend Dev',
286
- description: 'Chat | Files | Preview (3 columns)',
287
- icon: 'lucide:layout-template',
288
- // Layout: [Chat (33%) | Files (33%) | Preview (34%)]
321
+ id: 'three-columns',
322
+ name: 'Three Columns',
323
+ description: 'Three equal columns',
324
+ icon: 'lucide:columns-3',
289
325
  layout: createSplit(
290
326
  'vertical',
291
327
  33,
@@ -295,113 +331,66 @@ export const builtInPresets: LayoutPreset[] = [
295
331
  isCustom: false
296
332
  },
297
333
  {
298
- id: 'backend-dev',
299
- name: 'Backend Dev',
300
- description: 'Chat | Files | Terminal (3 columns)',
301
- icon: 'lucide:server',
302
- // Layout: [Chat (33%) | Files (33%) | Terminal (34%)]
303
- layout: createSplit(
304
- 'vertical',
305
- 33,
306
- createPanel('chat'),
307
- createSplit('vertical', 50, createPanel('files'), createPanel('terminal'))
308
- ),
309
- isCustom: false
310
- },
311
- {
312
- id: 'writing-mode',
313
- name: 'Writing Mode',
314
- description: 'Chat | Preview | Files',
315
- icon: 'lucide:file-edit',
316
- // Layout: [Chat (40%) | Preview (40%) | Files (20%)]
334
+ id: 'main-stack',
335
+ name: 'Main Stack',
336
+ description: 'Left column, stacked right',
337
+ icon: 'lucide:layout-panel-left',
317
338
  layout: createSplit(
318
339
  'vertical',
319
- 40,
340
+ 50,
320
341
  createPanel('chat'),
321
- createSplit('vertical', 66.7, createPanel('preview'), createPanel('files'))
322
- ),
323
- isCustom: false
324
- },
325
- {
326
- id: 'testing-mode',
327
- name: 'Testing Mode',
328
- description: 'Files | Preview | Terminal (stacked)',
329
- icon: 'lucide:flask-conical',
330
- // Layout: [Files (top 33%) | Preview (33%) | Terminal (34%)]
331
- layout: createSplit(
332
- 'horizontal',
333
- 33,
334
- createPanel('files'),
335
- createSplit('horizontal', 50, createPanel('preview'), createPanel('terminal'))
342
+ createSplit('horizontal', 50, createPanel('files'), createPanel('preview'))
336
343
  ),
337
344
  isCustom: false
338
345
  },
339
346
  {
340
- id: 'fullstack-lite',
341
- name: 'Full Stack Lite',
342
- description: 'Chat (top) | Files + Preview (bottom)',
343
- icon: 'lucide:layers',
344
- // Layout: [Chat (top 50%) | Files/Preview (bottom 50%)]
347
+ id: 'top-split',
348
+ name: 'Top Split',
349
+ description: 'Top row, two bottom columns',
350
+ icon: 'lucide:layout-panel-top',
345
351
  layout: createSplit(
346
352
  'horizontal',
347
353
  50,
348
- createPanel('chat'),
349
- createSplit('vertical', 50, createPanel('files'), createPanel('preview'))
350
- ),
351
- isCustom: false
352
- },
353
- {
354
- id: 'devops-mode',
355
- name: 'DevOps Mode',
356
- description: 'Terminal (big) | Chat + Files (side)',
357
- icon: 'lucide:workflow',
358
- // Layout: [Terminal (70%) | Chat/Files (30%)]
359
- layout: createSplit(
360
- 'vertical',
361
- 70,
362
- createPanel('terminal'),
363
- createSplit('horizontal', 50, createPanel('chat'), createPanel('files'))
354
+ createPanel('files'),
355
+ createSplit('vertical', 50, createPanel('chat'), createPanel('preview'))
364
356
  ),
365
357
  isCustom: false
366
358
  },
367
359
  {
368
- id: 'learning-mode',
369
- name: 'Learning Mode',
370
- description: 'Chat (big left) | Files + Preview (right)',
371
- icon: 'lucide:graduation-cap',
372
- // Layout: [Chat (50%) | Files/Preview (50%)]
360
+ id: 'sidebar-stack',
361
+ name: 'Side Stack',
362
+ description: 'Narrow left, two stacked right',
363
+ icon: 'lucide:panel-left-close',
373
364
  layout: createSplit(
374
365
  'vertical',
375
- 50,
366
+ 25,
376
367
  createPanel('chat'),
377
368
  createSplit('horizontal', 50, createPanel('files'), createPanel('preview'))
378
369
  ),
379
370
  isCustom: false
380
371
  },
372
+
373
+ // ============================================
374
+ // D. FOUR PANELS (3 presets)
375
+ // ============================================
381
376
  {
382
- id: 'git-workflow',
383
- name: 'Git Workflow',
384
- description: 'Git + Files + Terminal',
385
- icon: 'lucide:git-fork',
386
- // Layout: [Git (33%) | Files/Terminal (67%)]
377
+ id: 'quad-grid',
378
+ name: 'Quad Grid',
379
+ description: '2x2 grid layout',
380
+ icon: 'lucide:grid-2x2',
387
381
  layout: createSplit(
388
- 'vertical',
389
- 33,
390
- createPanel('git'),
391
- createSplit('horizontal', 50, createPanel('files'), createPanel('terminal'))
382
+ 'horizontal',
383
+ 50,
384
+ createSplit('vertical', 50, createPanel('chat'), createPanel('files')),
385
+ createSplit('vertical', 50, createPanel('preview'), createPanel('terminal'))
392
386
  ),
393
387
  isCustom: false
394
388
  },
395
-
396
- // ============================================
397
- // D. FOUR PANEL LAYOUTS (4 presets)
398
- // ============================================
399
389
  {
400
- id: 'full-dev',
401
- name: 'Full Development',
402
- description: 'All panels in optimal layout',
390
+ id: 'main-triple',
391
+ name: 'Main Triple',
392
+ description: 'Left column, three right panels',
403
393
  icon: 'lucide:layout-dashboard',
404
- // Layout: [Chat (33%) | Files (67%) / (Preview (50%) | Terminal (50%))]
405
394
  layout: createSplit(
406
395
  'vertical',
407
396
  33,
@@ -416,51 +405,17 @@ export const builtInPresets: LayoutPreset[] = [
416
405
  isCustom: false
417
406
  },
418
407
  {
419
- id: 'debug-mode',
420
- name: 'Debug Mode',
421
- description: 'Chat + Files/Preview + Terminal',
422
- icon: 'lucide:bug',
423
- // Layout: [Chat (35%) | Files/Preview (35%) | Terminal (30%)]
424
- layout: createSplit(
425
- 'vertical',
426
- 35,
427
- createPanel('chat'),
428
- createSplit(
429
- 'vertical',
430
- 53.8, // 35 / (35 + 30) * 100 ≈ 53.8%
431
- createSplit('horizontal', 60, createPanel('files'), createPanel('preview')),
432
- createPanel('terminal')
433
- )
434
- ),
435
- isCustom: false
436
- },
437
- {
438
- id: 'quad-grid',
439
- name: 'Quad Grid',
440
- description: '2x2 grid (25% each)',
441
- icon: 'lucide:grid-2x2',
442
- // Layout: [Chat/Files (top) | Preview/Terminal (bottom)]
443
- layout: createSplit(
444
- 'horizontal',
445
- 50,
446
- createSplit('vertical', 50, createPanel('chat'), createPanel('files')),
447
- createSplit('vertical', 50, createPanel('preview'), createPanel('terminal'))
448
- ),
449
- isCustom: false
450
- },
451
- {
452
- id: 'ide-classic',
453
- name: 'IDE Classic',
454
- description: 'Files (left narrow) | Chat/Preview/Terminal (right)',
455
- icon: 'lucide:panel-right',
456
- // Layout: [Files (20%) | Chat/Preview/Terminal (80%)]
408
+ id: 'sidebar-main-stack',
409
+ name: 'Classic IDE',
410
+ description: 'Three columns, right stacked',
411
+ icon: 'lucide:layout-template',
457
412
  layout: createSplit(
458
413
  'vertical',
459
414
  20,
460
415
  createPanel('files'),
461
416
  createSplit(
462
- 'horizontal',
463
- 33,
417
+ 'vertical',
418
+ 62.5, // 50 / (50 + 30) = 62.5%
464
419
  createPanel('chat'),
465
420
  createSplit('horizontal', 50, createPanel('preview'), createPanel('terminal'))
466
421
  )
@@ -469,14 +424,13 @@ export const builtInPresets: LayoutPreset[] = [
469
424
  },
470
425
 
471
426
  // ============================================
472
- // E. FIVE PANEL LAYOUTS (2 presets)
427
+ // E. FIVE PANELS (1 preset)
473
428
  // ============================================
474
429
  {
475
- id: 'full-dev-git',
476
- name: 'Full Dev + Git',
477
- description: 'All panels with source control',
478
- icon: 'lucide:layout-dashboard',
479
- // Layout: [Chat (25%) | Files/Git (top 50%) | Preview/Terminal (bottom 25%)]
430
+ id: 'full-grid',
431
+ name: 'Full Grid',
432
+ description: 'Left column, 2x2 right',
433
+ icon: 'lucide:layout-grid',
480
434
  layout: createSplit(
481
435
  'vertical',
482
436
  25,
@@ -489,26 +443,7 @@ export const builtInPresets: LayoutPreset[] = [
489
443
  )
490
444
  ),
491
445
  isCustom: false
492
- },
493
- {
494
- id: 'ide-git',
495
- name: 'IDE + Git',
496
- description: 'Classic IDE with source control',
497
- icon: 'lucide:git-branch',
498
- // Layout: [Git (20%) | Chat (top 50%) | Files/Terminal (bottom 50%)]
499
- layout: createSplit(
500
- 'vertical',
501
- 20,
502
- createPanel('git'),
503
- createSplit(
504
- 'horizontal',
505
- 50,
506
- createPanel('chat'),
507
- createSplit('vertical', 50, createPanel('files'), createPanel('terminal'))
508
- )
509
- ),
510
- isCustom: false
511
- },
446
+ }
512
447
  ];
513
448
 
514
449
  // ============================================
@@ -558,8 +493,16 @@ const defaultPanels: Record<PanelId, PanelConfig> = {
558
493
  }
559
494
  };
560
495
 
561
- // Default: Full Development layout
562
- const defaultFullDevPreset = builtInPresets.find((p) => p.id === 'full-dev')!;
496
+ export const PANEL_OPTIONS: { id: PanelId; title: string; icon: IconName }[] = [
497
+ { id: 'chat', title: 'AI Assistant', icon: 'lucide:bot' },
498
+ { id: 'files', title: 'Files', icon: 'lucide:folder' },
499
+ { id: 'preview', title: 'Preview', icon: 'lucide:globe' },
500
+ { id: 'terminal', title: 'Terminal', icon: 'lucide:terminal' },
501
+ { id: 'git', title: 'Source Control', icon: 'lucide:git-branch' }
502
+ ];
503
+
504
+ // Default: Main + Stack layout
505
+ const defaultPreset = builtInPresets.find((p) => p.id === 'main-stack')!;
563
506
 
564
507
  // ============================================
565
508
  // CORE STATE
@@ -567,8 +510,8 @@ const defaultFullDevPreset = builtInPresets.find((p) => p.id === 'full-dev')!;
567
510
 
568
511
  export const workspaceState = $state<WorkspaceState>({
569
512
  panels: { ...defaultPanels },
570
- layout: defaultFullDevPreset.layout,
571
- activePresetId: 'full-dev',
513
+ layout: defaultPreset.layout,
514
+ activePresetId: 'main-stack',
572
515
  navigatorCollapsed: false,
573
516
  navigatorWidth: 220,
574
517
  activeMobilePanel: 'chat'
@@ -633,6 +576,127 @@ export function setSplitRatio(path: number[], ratio: number): void {
633
576
  debug.log('workspace', `Split ratio updated at path ${path.join('.')}: ${clampedRatio}%`);
634
577
  }
635
578
 
579
+ // ============================================
580
+ // PANEL MANIPULATION
581
+ // ============================================
582
+
583
+ /**
584
+ * Swap a panel's content with another panel type.
585
+ * If the new panel is already visible, swap their positions.
586
+ */
587
+ export function swapPanel(currentPanelId: PanelId, newPanelId: PanelId): void {
588
+ if (currentPanelId === newPanelId) return;
589
+
590
+ const visiblePanels = getVisiblePanels(workspaceState.layout);
591
+
592
+ if (visiblePanels.includes(newPanelId)) {
593
+ workspaceState.layout = swapPanelsInTree(workspaceState.layout, currentPanelId, newPanelId);
594
+ } else {
595
+ workspaceState.layout = updatePanelInTree(workspaceState.layout, currentPanelId, newPanelId);
596
+ }
597
+
598
+ workspaceState.activePresetId = undefined;
599
+ saveWorkspaceState();
600
+ debug.log('workspace', `Swapped panel ${currentPanelId} → ${newPanelId}`);
601
+ }
602
+
603
+ /**
604
+ * Split a panel into two, with the original panel on the first side
605
+ * and a new panel (or empty slot) on the second side.
606
+ */
607
+ export function splitPanel(
608
+ panelId: PanelId,
609
+ direction: SplitDirection,
610
+ newPanelId: PanelId | null = null
611
+ ): void {
612
+ workspaceState.layout = splitPanelInTree(workspaceState.layout, panelId, direction, newPanelId);
613
+ workspaceState.activePresetId = undefined;
614
+ saveWorkspaceState();
615
+ debug.log('workspace', `Split panel ${panelId} ${direction} with ${newPanelId ?? 'empty'}`);
616
+ }
617
+
618
+ /**
619
+ * Close a panel and collapse its parent split.
620
+ * Returns false if the panel is the last one (minimum 1 panel required).
621
+ */
622
+ export function closePanel(panelId: PanelId): boolean {
623
+ const visiblePanels = getVisiblePanels(workspaceState.layout);
624
+ if (visiblePanels.length <= 1) {
625
+ debug.log('workspace', 'Cannot close last panel');
626
+ return false;
627
+ }
628
+
629
+ const result = removePanelFromTree(workspaceState.layout, panelId);
630
+ if (result) {
631
+ workspaceState.layout = result;
632
+ workspaceState.activePresetId = undefined;
633
+ saveWorkspaceState();
634
+ debug.log('workspace', `Closed panel ${panelId}`);
635
+ return true;
636
+ }
637
+ return false;
638
+ }
639
+
640
+ /**
641
+ * Check if a panel can be closed (more than 1 panel visible)
642
+ */
643
+ export function canClosePanel(): boolean {
644
+ return getVisiblePanels(workspaceState.layout).length > 1;
645
+ }
646
+
647
+ /**
648
+ * Set the panel type at a specific path (used for empty slot panel picker).
649
+ * If the panel is already visible elsewhere, it gets moved here
650
+ * and the old position is cleaned up.
651
+ */
652
+ export function setPanelAtPath(path: number[], panelId: PanelId): void {
653
+ let layout = workspaceState.layout;
654
+ const visiblePanels = getVisiblePanels(layout);
655
+
656
+ if (visiblePanels.includes(panelId)) {
657
+ // Panel already visible — move it here, clean up old position
658
+ layout = updatePanelInTree(layout, panelId, null);
659
+ layout = setNodeAtPath(layout, path, createPanel(panelId));
660
+ const cleaned = cleanupEmptyNodes(layout);
661
+ if (cleaned) layout = cleaned;
662
+ } else {
663
+ layout = setNodeAtPath(layout, path, createPanel(panelId));
664
+ }
665
+
666
+ workspaceState.layout = layout;
667
+ workspaceState.activePresetId = undefined;
668
+ saveWorkspaceState();
669
+ debug.log('workspace', `Set panel at path ${path.join('.')} to ${panelId}`);
670
+ }
671
+
672
+ /**
673
+ * Close a panel at a specific path by collapsing its parent split.
674
+ * The sibling takes the parent's place. Used for empty slot cancel.
675
+ */
676
+ export function closePanelAtPath(path: number[]): boolean {
677
+ if (path.length === 0) {
678
+ // Root node — can't close the only thing in the layout
679
+ return false;
680
+ }
681
+
682
+ const totalLeaves = countLeafNodes(workspaceState.layout);
683
+ if (totalLeaves <= 1) return false;
684
+
685
+ const parentPath = path.slice(0, -1);
686
+ const childIndex = path[path.length - 1];
687
+ const siblingIndex = childIndex === 0 ? 1 : 0;
688
+
689
+ const parentNode = getNodeAtPath(workspaceState.layout, parentPath);
690
+ if (!parentNode || parentNode.type !== 'split') return false;
691
+
692
+ const sibling = parentNode.children[siblingIndex];
693
+ workspaceState.layout = setNodeAtPath(workspaceState.layout, parentPath, sibling);
694
+ workspaceState.activePresetId = undefined;
695
+ saveWorkspaceState();
696
+ debug.log('workspace', `Closed panel at path ${path.join('.')}`);
697
+ return true;
698
+ }
699
+
636
700
  // ============================================
637
701
  // LAYOUT PRESETS
638
702
  // ============================================
@@ -655,29 +719,8 @@ export function applyLayoutPreset(preset: LayoutPreset): void {
655
719
  }
656
720
 
657
721
  export function resetToDefault(): void {
658
- applyLayoutPreset(defaultFullDevPreset);
659
- debug.log('workspace', 'Reset to default layout (Full Development)');
660
- }
661
-
662
- // Shortcuts for built-in presets
663
- export function applyFocusChatLayout(): void {
664
- const preset = builtInPresets.find((p) => p.id === 'focus-chat')!;
665
- applyLayoutPreset(preset);
666
- }
667
-
668
- export function applyCodeReviewLayout(): void {
669
- const preset = builtInPresets.find((p) => p.id === 'code-review')!;
670
- applyLayoutPreset(preset);
671
- }
672
-
673
- export function applyFullDevLayout(): void {
674
- const preset = builtInPresets.find((p) => p.id === 'full-dev')!;
675
- applyLayoutPreset(preset);
676
- }
677
-
678
- export function applyDebugLayout(): void {
679
- const preset = builtInPresets.find((p) => p.id === 'debug-mode')!;
680
- applyLayoutPreset(preset);
722
+ applyLayoutPreset(defaultPreset);
723
+ debug.log('workspace', 'Reset to default layout (Main + Stack)');
681
724
  }
682
725
 
683
726
  // ============================================