@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.
- package/CONTRIBUTING.md +40 -355
- package/README.md +46 -113
- package/backend/lib/engine/adapters/opencode/message-converter.ts +53 -2
- package/backend/lib/engine/adapters/opencode/stream.ts +89 -5
- package/backend/lib/mcp/config.ts +7 -3
- package/backend/lib/mcp/servers/helper.ts +25 -14
- package/backend/lib/mcp/servers/index.ts +7 -2
- package/backend/lib/project/status-manager.ts +221 -181
- package/frontend/lib/components/chat/ChatInterface.svelte +7 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +16 -9
- package/frontend/lib/components/chat/message/ChatMessages.svelte +16 -4
- package/frontend/lib/components/chat/tools/AgentTool.svelte +12 -11
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +3 -3
- package/frontend/lib/components/chat/tools/BashTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +3 -1
- package/frontend/lib/components/chat/tools/EditTool.svelte +6 -6
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/GlobTool.svelte +12 -12
- package/frontend/lib/components/chat/tools/GrepTool.svelte +5 -5
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +6 -6
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +2 -2
- package/frontend/lib/components/chat/tools/ReadTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/TaskStopTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/TaskTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +4 -4
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +1 -1
- package/frontend/lib/components/chat/tools/WriteTool.svelte +3 -3
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +3 -3
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +2 -2
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +1 -1
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +2 -2
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +1 -1
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +5 -5
- package/frontend/lib/components/common/Button.svelte +1 -1
- package/frontend/lib/components/common/Card.svelte +3 -3
- package/frontend/lib/components/common/Input.svelte +3 -3
- package/frontend/lib/components/common/LoadingSpinner.svelte +1 -1
- package/frontend/lib/components/common/Select.svelte +6 -6
- package/frontend/lib/components/common/Textarea.svelte +3 -3
- package/frontend/lib/components/files/FileViewer.svelte +1 -1
- package/frontend/lib/components/git/ChangesSection.svelte +2 -4
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +9 -29
- package/frontend/lib/components/preview/browser/components/Container.svelte +17 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +2 -2
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +0 -6
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +15 -15
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +2 -2
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +380 -383
- package/frontend/lib/components/workspace/MobileNavigator.svelte +391 -395
- package/frontend/lib/components/workspace/PanelHeader.svelte +623 -505
- package/frontend/lib/components/workspace/ViewMenu.svelte +9 -25
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +29 -4
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +77 -86
- package/frontend/lib/services/project/status.service.ts +160 -159
- package/frontend/lib/stores/ui/workspace.svelte.ts +326 -283
- package/package.json +1 -1
- package/scripts/pre-publish-check.sh +0 -142
- package/scripts/setup-hooks.sh +0 -134
- package/scripts/validate-branch-name.sh +0 -47
- 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
|
|
278
|
+
// A. SINGLE PANEL (1 preset)
|
|
156
279
|
// ============================================
|
|
157
280
|
{
|
|
158
|
-
id: 'focus
|
|
159
|
-
name: 'Focus
|
|
160
|
-
description: '
|
|
161
|
-
icon: 'lucide:
|
|
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
|
|
290
|
+
// B. TWO PANELS (3 presets)
|
|
200
291
|
// ============================================
|
|
201
292
|
{
|
|
202
|
-
id: '
|
|
203
|
-
name: '
|
|
204
|
-
description: '
|
|
205
|
-
icon: 'lucide:
|
|
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: '
|
|
211
|
-
name: '
|
|
212
|
-
description: '
|
|
213
|
-
icon: 'lucide:
|
|
214
|
-
layout: createSplit('
|
|
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: '
|
|
227
|
-
name: '
|
|
228
|
-
description: '
|
|
309
|
+
id: 'sidebar-main',
|
|
310
|
+
name: 'Sidebar',
|
|
311
|
+
description: 'Narrow left, wide right',
|
|
229
312
|
icon: 'lucide:panel-left',
|
|
230
|
-
layout: createSplit('vertical',
|
|
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
|
|
318
|
+
// C. THREE PANELS (4 presets)
|
|
268
319
|
// ============================================
|
|
269
320
|
{
|
|
270
|
-
id: '
|
|
271
|
-
name: '
|
|
272
|
-
description: '
|
|
273
|
-
icon: 'lucide:
|
|
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: '
|
|
299
|
-
name: '
|
|
300
|
-
description: '
|
|
301
|
-
icon: 'lucide:
|
|
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
|
-
|
|
340
|
+
50,
|
|
320
341
|
createPanel('chat'),
|
|
321
|
-
createSplit('
|
|
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: '
|
|
341
|
-
name: '
|
|
342
|
-
description: '
|
|
343
|
-
icon: 'lucide:
|
|
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('
|
|
349
|
-
createSplit('vertical', 50, createPanel('
|
|
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: '
|
|
369
|
-
name: '
|
|
370
|
-
description: '
|
|
371
|
-
icon: 'lucide:
|
|
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
|
-
|
|
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: '
|
|
383
|
-
name: '
|
|
384
|
-
description: '
|
|
385
|
-
icon: 'lucide:
|
|
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
|
-
'
|
|
389
|
-
|
|
390
|
-
createPanel('
|
|
391
|
-
createSplit('
|
|
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: '
|
|
401
|
-
name: '
|
|
402
|
-
description: '
|
|
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: '
|
|
420
|
-
name: '
|
|
421
|
-
description: '
|
|
422
|
-
icon: 'lucide:
|
|
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
|
-
'
|
|
463
|
-
|
|
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
|
|
427
|
+
// E. FIVE PANELS (1 preset)
|
|
473
428
|
// ============================================
|
|
474
429
|
{
|
|
475
|
-
id: 'full-
|
|
476
|
-
name: 'Full
|
|
477
|
-
description: '
|
|
478
|
-
icon: 'lucide:layout-
|
|
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
|
-
|
|
562
|
-
|
|
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:
|
|
571
|
-
activePresetId: '
|
|
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(
|
|
659
|
-
debug.log('workspace', 'Reset to default layout (
|
|
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
|
// ============================================
|