claudedesk 4.3.1 → 4.4.0

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 (124) hide show
  1. package/.github/workflows/ci.yml +44 -2
  2. package/CLAUDE.md +36 -3
  3. package/PHASE_1_IMPLEMENTATION.md +313 -0
  4. package/PHASE_2_PARTIAL_IMPLEMENTATION.md +286 -0
  5. package/dist/main/cli-manager.js +67 -2
  6. package/dist/main/command-registry.js +196 -0
  7. package/dist/main/git-manager.js +841 -0
  8. package/dist/main/index.js +25 -1
  9. package/dist/main/ipc-handlers.js +347 -3
  10. package/dist/main/layout-presets-manager.js +233 -0
  11. package/dist/main/model-history-manager.js +187 -0
  12. package/dist/main/session-manager.js +83 -26
  13. package/dist/main/session-persistence.js +1 -0
  14. package/dist/main/session-pool.js +40 -9
  15. package/dist/main/settings-persistence.js +67 -12
  16. package/dist/renderer/assets/index-BNeYLqV4.css +32 -0
  17. package/dist/renderer/assets/index-D5O5Ljoo.js +17189 -0
  18. package/dist/renderer/index.html +2 -2
  19. package/dist/shared/ipc-contract.js +79 -0
  20. package/dist/shared/model-detector.js +83 -0
  21. package/dist/shared/types/command-types.js +5 -0
  22. package/dist/shared/types/git-types.js +2 -0
  23. package/dist/types/layout-presets.js +11 -0
  24. package/docs/git-integration-implementation-tasks.md +974 -0
  25. package/docs/git-integration-product-spec.md +916 -0
  26. package/docs/git-integration-ui-spec.md +1464 -0
  27. package/docs/repo-index.md +83 -8
  28. package/e2e/app-launch.spec.ts +31 -0
  29. package/e2e/fixtures/electron.ts +34 -0
  30. package/e2e/keyboard-shortcuts.spec.ts +50 -0
  31. package/e2e/session.spec.ts +34 -0
  32. package/e2e/split-view.spec.ts +21 -0
  33. package/package.json +16 -3
  34. package/playwright.config.ts +15 -0
  35. package/src/main/cli-manager.ts +74 -3
  36. package/src/main/command-registry.ts +221 -0
  37. package/src/main/git-manager.test.ts +374 -0
  38. package/src/main/git-manager.ts +909 -0
  39. package/src/main/index.ts +31 -1
  40. package/src/main/ipc-emitter.test.ts +60 -0
  41. package/src/main/ipc-handlers.ts +295 -3
  42. package/src/main/ipc-registry.test.ts +75 -0
  43. package/src/main/layout-presets-manager.ts +268 -0
  44. package/src/main/model-history-manager.ts +196 -0
  45. package/src/main/session-manager.ts +102 -30
  46. package/src/main/session-persistence.test.ts +215 -0
  47. package/src/main/session-persistence.ts +1 -0
  48. package/src/main/session-pool.ts +31 -9
  49. package/src/main/settings-persistence.ts +74 -12
  50. package/src/renderer/App.tsx +215 -43
  51. package/src/renderer/components/CustomLayoutBuilder.tsx +143 -0
  52. package/src/renderer/components/GitPanel.test.tsx +181 -0
  53. package/src/renderer/components/GitPanel.tsx +1407 -0
  54. package/src/renderer/components/LayoutPicker.tsx +182 -0
  55. package/src/renderer/components/LayoutPreviewCard.tsx +175 -0
  56. package/src/renderer/components/ModelHistoryPanel.tsx +435 -0
  57. package/src/renderer/components/PaneHeader.test.tsx +96 -0
  58. package/src/renderer/components/PaneHeader.tsx +28 -0
  59. package/src/renderer/components/SplitLayout.test.tsx +153 -0
  60. package/src/renderer/components/SplitLayout.tsx +36 -1
  61. package/src/renderer/components/Terminal.tsx +10 -10
  62. package/src/renderer/components/WelcomeWizard.tsx +143 -0
  63. package/src/renderer/components/WizardStepper.tsx +135 -0
  64. package/src/renderer/components/ui/ClaudeReadinessProgress.tsx +168 -0
  65. package/src/renderer/components/ui/CommitDialog.test.tsx +134 -0
  66. package/src/renderer/components/ui/CommitDialog.tsx +464 -0
  67. package/src/renderer/components/ui/EmptyState.test.tsx +87 -0
  68. package/src/renderer/components/ui/EmptyState.tsx +115 -86
  69. package/src/renderer/components/ui/FeatureShowcase.tsx +187 -0
  70. package/src/renderer/components/ui/FuelGaugeBar.tsx +59 -0
  71. package/src/renderer/components/ui/FuelStatusIndicator.tsx +358 -0
  72. package/src/renderer/components/ui/FuelTooltip.tsx +267 -0
  73. package/src/renderer/components/ui/HelpButton.tsx +43 -0
  74. package/src/renderer/components/ui/ModelBadge.tsx +72 -0
  75. package/src/renderer/components/ui/ModelSwitcher.tsx +180 -0
  76. package/src/renderer/components/ui/PanelFooter.tsx +90 -0
  77. package/src/renderer/components/ui/PanelHeader.tsx +87 -0
  78. package/src/renderer/components/ui/PanelHelpOverlay.tsx +274 -0
  79. package/src/renderer/components/ui/QuickActionCard.tsx +103 -0
  80. package/src/renderer/components/ui/RecentSessionsList.tsx +154 -0
  81. package/src/renderer/components/ui/SessionStatusIndicator.tsx +104 -0
  82. package/src/renderer/components/ui/SettingsDialog.tsx +94 -0
  83. package/src/renderer/components/ui/ShortcutsPanel.tsx +433 -0
  84. package/src/renderer/components/ui/StatusPopover.tsx +344 -0
  85. package/src/renderer/components/ui/TabBar.test.tsx +124 -0
  86. package/src/renderer/components/ui/TabBar.tsx +152 -168
  87. package/src/renderer/components/ui/ToolbarDropdown.tsx +227 -0
  88. package/src/renderer/components/ui/ToolsDropdown.tsx +119 -0
  89. package/src/renderer/components/ui/TooltipCoach.tsx +217 -0
  90. package/src/renderer/components/ui/WelcomeHero.tsx +85 -0
  91. package/src/renderer/components/ui/index.ts +5 -0
  92. package/src/renderer/components/wizard/Step1_Welcome.tsx +166 -0
  93. package/src/renderer/components/wizard/Step2_LayoutPicker.tsx +246 -0
  94. package/src/renderer/components/wizard/Step3_Features.tsx +278 -0
  95. package/src/renderer/components/wizard/Step4_Ready.tsx +279 -0
  96. package/src/renderer/hooks/useGit.test.ts +140 -0
  97. package/src/renderer/hooks/useGit.ts +395 -0
  98. package/src/renderer/hooks/useLayoutPicker.ts +77 -0
  99. package/src/renderer/hooks/useModelHistory.ts +69 -0
  100. package/src/renderer/hooks/useSessionManager.test.ts +146 -0
  101. package/src/renderer/hooks/useSessionManager.ts +5 -0
  102. package/src/renderer/hooks/useSplitView.test.ts +168 -0
  103. package/src/renderer/hooks/useSplitView.ts +126 -128
  104. package/src/renderer/styles/globals.css +505 -0
  105. package/src/renderer/utils/fuzzy-search.test.ts +121 -0
  106. package/src/renderer/utils/layout-tree.test.ts +310 -0
  107. package/src/renderer/utils/layout-tree.ts +170 -0
  108. package/src/renderer/utils/variable-resolver.test.ts +102 -0
  109. package/src/shared/ipc-contract.ts +157 -0
  110. package/src/shared/ipc-types.ts +52 -1
  111. package/src/shared/message-parser.test.ts +79 -0
  112. package/src/shared/model-detector.test.ts +90 -0
  113. package/src/shared/model-detector.ts +97 -0
  114. package/src/shared/types/command-types.ts +26 -0
  115. package/src/shared/types/git-types.ts +126 -0
  116. package/src/types/layout-presets.ts +22 -0
  117. package/test/helpers/electron-api-mock.ts +52 -0
  118. package/test/setup-main.ts +61 -0
  119. package/test/setup-renderer.ts +8 -0
  120. package/tsconfig.json +1 -0
  121. package/tsconfig.main.json +2 -1
  122. package/vitest.workspace.ts +37 -0
  123. package/dist/renderer/assets/index-CR22a7j2.css +0 -32
  124. package/dist/renderer/assets/index-Dp-eceNq.js +0 -13915
@@ -33,6 +33,8 @@ import type {
33
33
  TeammateDetectedEvent,
34
34
  TasksUpdatedEvent,
35
35
  TeamRemovedEvent,
36
+ ClaudeModel,
37
+ ModelSwitchEvent,
36
38
  } from './ipc-types';
37
39
 
38
40
  import type {
@@ -64,6 +66,23 @@ import type {
64
66
  AtlasScanProgress,
65
67
  } from './types/atlas-types';
66
68
 
69
+ import type {
70
+ LayoutPreset,
71
+ } from '../types/layout-presets';
72
+
73
+ import type { LayoutNode } from './ipc-types';
74
+
75
+ import type {
76
+ GitStatus,
77
+ GitBranchInfo,
78
+ GitCommitInfo,
79
+ GitDiffResult,
80
+ GitOperationResult,
81
+ GitCommitRequest,
82
+ GeneratedCommitMessage,
83
+ GitRemoteProgress,
84
+ } from './types/git-types';
85
+
67
86
  // ─── Contract helper types ──────────────────────────────────────────
68
87
 
69
88
  /** renderer → main, expects a return value (ipcRenderer.invoke / ipcMain.handle) */
@@ -107,6 +126,13 @@ export interface IPCContractMap {
107
126
  resizeSession: SendContract<'session:resize', [SessionResizeRequest]>;
108
127
  sessionReady: SendContract<'session:ready', [string]>;
109
128
 
129
+ // ── Model switching (invoke) ──
130
+ switchModel: InvokeContract<'model:switch', [string, ClaudeModel], boolean>;
131
+
132
+ // ── Model History (invoke) ──
133
+ getModelHistory: InvokeContract<'model:getHistory', [string], import('../main/model-history-manager').ModelSwitchHistoryEntry[]>;
134
+ clearModelHistory: InvokeContract<'model:clearHistory', [string], boolean>;
135
+
110
136
  // ── Window controls (send) ──
111
137
  minimizeWindow: SendContract<'window:minimize', []>;
112
138
  maximizeWindow: SendContract<'window:maximize', []>;
@@ -119,6 +145,7 @@ export interface IPCContractMap {
119
145
  onSessionUpdated: EventContract<'session:updated', SessionMetadata>;
120
146
  onSessionOutput: EventContract<'session:output', SessionOutput>;
121
147
  onSessionExited: EventContract<'session:exited', SessionExitEvent>;
148
+ onModelChanged: EventContract<'model:changed', ModelSwitchEvent>;
122
149
 
123
150
  // ── Dialogs & File system (invoke) ──
124
151
  browseDirectory: InvokeContract<'dialog:browseDirectory', [], string | null>;
@@ -192,6 +219,8 @@ export interface IPCContractMap {
192
219
  unlinkSessionFromTeam: InvokeContract<'teams:unlinkSession', [string], boolean>;
193
220
  closeTeam: InvokeContract<'teams:close', [string], boolean>;
194
221
  updateAutoLayoutTeams: InvokeContract<'settings:updateAutoLayout', [boolean], boolean>;
222
+ updateUIMode: InvokeContract<'settings:updateUIMode', ['beginner' | 'expert'], boolean>;
223
+ updateDefaultModel: InvokeContract<'settings:updateDefaultModel', [ClaudeModel], boolean>;
195
224
 
196
225
  // ── Agent Teams events (main→renderer) ──
197
226
  onTeamDetected: EventContract<'teams:detected', TeamInfo>;
@@ -206,9 +235,47 @@ export interface IPCContractMap {
206
235
  getAtlasSettings: InvokeContract<'atlas:getSettings', [], AtlasSettings>;
207
236
  updateAtlasSettings: InvokeContract<'atlas:updateSettings', [Partial<AtlasSettings>], AtlasSettings>;
208
237
 
238
+ // ── Command Registry (invoke) ──
239
+ searchCommands: InvokeContract<'commands:search', [string, number?], import('./types/command-types').CommandSearchResult[]>;
240
+ getAllCommands: InvokeContract<'commands:getAll', [], import('./types/command-types').CommandRegistryData>;
241
+ executeCommand: InvokeContract<'commands:execute', [string, any[]?], boolean>;
242
+
209
243
  // ── Repository Atlas events (main→renderer) ──
210
244
  onAtlasScanProgress: EventContract<'atlas:scanProgress', AtlasScanProgress>;
211
245
 
246
+ // ── Layout Presets (invoke) ──
247
+ getLayoutPresets: InvokeContract<'layout:getPresets', [], LayoutPreset[]>;
248
+ applyLayoutPreset: InvokeContract<'layout:apply', [string], boolean>;
249
+ applyCustomLayout: InvokeContract<'layout:applyCustom', [number, number], boolean>;
250
+ getCurrentLayout: InvokeContract<'layout:getCurrent', [], LayoutNode>;
251
+
252
+ // ── Git Integration (invoke) ──
253
+ getGitStatus: InvokeContract<'git:status', [string], GitStatus>;
254
+ getGitBranches: InvokeContract<'git:branches', [string], GitBranchInfo[]>;
255
+ gitStageFiles: InvokeContract<'git:stage', [string, string[]], GitOperationResult>;
256
+ gitUnstageFiles: InvokeContract<'git:unstage', [string, string[]], GitOperationResult>;
257
+ gitStageAll: InvokeContract<'git:stageAll', [string], GitOperationResult>;
258
+ gitUnstageAll: InvokeContract<'git:unstageAll', [string], GitOperationResult>;
259
+ gitCommit: InvokeContract<'git:commit', [GitCommitRequest], GitOperationResult>;
260
+ gitGenerateMessage: InvokeContract<'git:generateMessage', [string], GeneratedCommitMessage>;
261
+ gitPush: InvokeContract<'git:push', [string, boolean?], GitOperationResult>;
262
+ gitPull: InvokeContract<'git:pull', [string], GitOperationResult>;
263
+ gitFetch: InvokeContract<'git:fetch', [string], GitOperationResult>;
264
+ gitSwitchBranch: InvokeContract<'git:switchBranch', [string, string], GitOperationResult>;
265
+ gitCreateBranch: InvokeContract<'git:createBranch', [string, string], GitOperationResult>;
266
+ gitLog: InvokeContract<'git:log', [string, number?], GitCommitInfo[]>;
267
+ gitDiff: InvokeContract<'git:diff', [string, string, boolean], GitDiffResult>;
268
+ gitCommitDiff: InvokeContract<'git:commitDiff', [string, string], GitCommitInfo>;
269
+ gitDiscardFile: InvokeContract<'git:discardFile', [string, string], GitOperationResult>;
270
+ gitDiscardAll: InvokeContract<'git:discardAll', [string], GitOperationResult>;
271
+ gitInit: InvokeContract<'git:init', [string], GitOperationResult>;
272
+ gitStartWatching: InvokeContract<'git:startWatching', [string], boolean>;
273
+ gitStopWatching: InvokeContract<'git:stopWatching', [string], boolean>;
274
+
275
+ // ── Git events (main→renderer) ──
276
+ onGitStatusChanged: EventContract<'git:statusChanged', GitStatus>;
277
+ onGitRemoteProgress: EventContract<'git:remoteProgress', GitRemoteProgress>;
278
+
212
279
  // ── App info (invoke) ──
213
280
  getVersionInfo: InvokeContract<'app:getVersionInfo', [], AppVersionInfo>;
214
281
  }
@@ -233,6 +300,13 @@ export const channels: { [K in keyof IPCContractMap]: ChannelOf<K> } = {
233
300
  resizeSession: 'session:resize',
234
301
  sessionReady: 'session:ready',
235
302
 
303
+ // Model switching
304
+ switchModel: 'model:switch',
305
+
306
+ // Model History
307
+ getModelHistory: 'model:getHistory',
308
+ clearModelHistory: 'model:clearHistory',
309
+
236
310
  // Window controls
237
311
  minimizeWindow: 'window:minimize',
238
312
  maximizeWindow: 'window:maximize',
@@ -245,6 +319,7 @@ export const channels: { [K in keyof IPCContractMap]: ChannelOf<K> } = {
245
319
  onSessionUpdated: 'session:updated',
246
320
  onSessionOutput: 'session:output',
247
321
  onSessionExited: 'session:exited',
322
+ onModelChanged: 'model:changed',
248
323
 
249
324
  // Dialogs
250
325
  browseDirectory: 'dialog:browseDirectory',
@@ -318,6 +393,8 @@ export const channels: { [K in keyof IPCContractMap]: ChannelOf<K> } = {
318
393
  unlinkSessionFromTeam: 'teams:unlinkSession',
319
394
  closeTeam: 'teams:close',
320
395
  updateAutoLayoutTeams: 'settings:updateAutoLayout',
396
+ updateUIMode: 'settings:updateUIMode',
397
+ updateDefaultModel: 'settings:updateDefaultModel',
321
398
 
322
399
  // Agent Teams events
323
400
  onTeamDetected: 'teams:detected',
@@ -332,9 +409,47 @@ export const channels: { [K in keyof IPCContractMap]: ChannelOf<K> } = {
332
409
  getAtlasSettings: 'atlas:getSettings',
333
410
  updateAtlasSettings: 'atlas:updateSettings',
334
411
 
412
+ // Command Registry
413
+ searchCommands: 'commands:search',
414
+ getAllCommands: 'commands:getAll',
415
+ executeCommand: 'commands:execute',
416
+
335
417
  // Repository Atlas events
336
418
  onAtlasScanProgress: 'atlas:scanProgress',
337
419
 
420
+ // Layout Presets
421
+ getLayoutPresets: 'layout:getPresets',
422
+ applyLayoutPreset: 'layout:apply',
423
+ applyCustomLayout: 'layout:applyCustom',
424
+ getCurrentLayout: 'layout:getCurrent',
425
+
426
+ // Git Integration
427
+ getGitStatus: 'git:status',
428
+ getGitBranches: 'git:branches',
429
+ gitStageFiles: 'git:stage',
430
+ gitUnstageFiles: 'git:unstage',
431
+ gitStageAll: 'git:stageAll',
432
+ gitUnstageAll: 'git:unstageAll',
433
+ gitCommit: 'git:commit',
434
+ gitGenerateMessage: 'git:generateMessage',
435
+ gitPush: 'git:push',
436
+ gitPull: 'git:pull',
437
+ gitFetch: 'git:fetch',
438
+ gitSwitchBranch: 'git:switchBranch',
439
+ gitCreateBranch: 'git:createBranch',
440
+ gitLog: 'git:log',
441
+ gitDiff: 'git:diff',
442
+ gitCommitDiff: 'git:commitDiff',
443
+ gitDiscardFile: 'git:discardFile',
444
+ gitDiscardAll: 'git:discardAll',
445
+ gitInit: 'git:init',
446
+ gitStartWatching: 'git:startWatching',
447
+ gitStopWatching: 'git:stopWatching',
448
+
449
+ // Git events
450
+ onGitStatusChanged: 'git:statusChanged',
451
+ onGitRemoteProgress: 'git:remoteProgress',
452
+
338
453
  // App info
339
454
  getVersionInfo: 'app:getVersionInfo',
340
455
  };
@@ -357,6 +472,11 @@ export const contractKinds: { [K in keyof IPCContractMap]: KindOf<K> } = {
357
472
  resizeSession: 'send',
358
473
  sessionReady: 'send',
359
474
 
475
+ switchModel: 'invoke',
476
+
477
+ getModelHistory: 'invoke',
478
+ clearModelHistory: 'invoke',
479
+
360
480
  minimizeWindow: 'send',
361
481
  maximizeWindow: 'send',
362
482
  closeWindow: 'send',
@@ -367,6 +487,7 @@ export const contractKinds: { [K in keyof IPCContractMap]: KindOf<K> } = {
367
487
  onSessionUpdated: 'event',
368
488
  onSessionOutput: 'event',
369
489
  onSessionExited: 'event',
490
+ onModelChanged: 'event',
370
491
 
371
492
  browseDirectory: 'invoke',
372
493
  showSaveDialog: 'invoke',
@@ -397,6 +518,10 @@ export const contractKinds: { [K in keyof IPCContractMap]: KindOf<K> } = {
397
518
  updateTemplate: 'invoke',
398
519
  deleteTemplate: 'invoke',
399
520
 
521
+ searchCommands: 'invoke',
522
+ getAllCommands: 'invoke',
523
+ executeCommand: 'invoke',
524
+
400
525
  getFileInfo: 'invoke',
401
526
  readFileContent: 'invoke',
402
527
 
@@ -429,6 +554,8 @@ export const contractKinds: { [K in keyof IPCContractMap]: KindOf<K> } = {
429
554
  unlinkSessionFromTeam: 'invoke',
430
555
  closeTeam: 'invoke',
431
556
  updateAutoLayoutTeams: 'invoke',
557
+ updateUIMode: 'invoke',
558
+ updateDefaultModel: 'invoke',
432
559
 
433
560
  onTeamDetected: 'event',
434
561
  onTeammateAdded: 'event',
@@ -443,6 +570,36 @@ export const contractKinds: { [K in keyof IPCContractMap]: KindOf<K> } = {
443
570
 
444
571
  onAtlasScanProgress: 'event',
445
572
 
573
+ getLayoutPresets: 'invoke',
574
+ applyLayoutPreset: 'invoke',
575
+ applyCustomLayout: 'invoke',
576
+ getCurrentLayout: 'invoke',
577
+
578
+ // Git Integration
579
+ getGitStatus: 'invoke',
580
+ getGitBranches: 'invoke',
581
+ gitStageFiles: 'invoke',
582
+ gitUnstageFiles: 'invoke',
583
+ gitStageAll: 'invoke',
584
+ gitUnstageAll: 'invoke',
585
+ gitCommit: 'invoke',
586
+ gitGenerateMessage: 'invoke',
587
+ gitPush: 'invoke',
588
+ gitPull: 'invoke',
589
+ gitFetch: 'invoke',
590
+ gitSwitchBranch: 'invoke',
591
+ gitCreateBranch: 'invoke',
592
+ gitLog: 'invoke',
593
+ gitDiff: 'invoke',
594
+ gitCommitDiff: 'invoke',
595
+ gitDiscardFile: 'invoke',
596
+ gitDiscardAll: 'invoke',
597
+ gitInit: 'invoke',
598
+ gitStartWatching: 'invoke',
599
+ gitStopWatching: 'invoke',
600
+ onGitStatusChanged: 'event',
601
+ onGitRemoteProgress: 'event',
602
+
446
603
  getVersionInfo: 'invoke',
447
604
  };
448
605
 
@@ -4,11 +4,18 @@ export type PermissionMode = 'standard' | 'skip-permissions';
4
4
  // Session status
5
5
  export type SessionStatus = 'starting' | 'running' | 'exited' | 'error';
6
6
 
7
+ // Claude model types
8
+ export type ClaudeModel = 'sonnet' | 'opus' | 'haiku' | 'auto';
9
+
10
+ // Model preset types
11
+ export type ModelPreset = 'cheap' | 'balanced' | 'power';
12
+
7
13
  // Session creation request
8
14
  export interface SessionCreateRequest {
9
15
  name?: string;
10
16
  workingDirectory: string;
11
17
  permissionMode: PermissionMode;
18
+ model?: ClaudeModel; // Starting model override (defaults to AppSettings.defaultModel)
12
19
  }
13
20
 
14
21
  // Session metadata
@@ -24,6 +31,7 @@ export interface SessionMetadata {
24
31
  agentId?: string;
25
32
  agentType?: 'lead' | 'teammate';
26
33
  isTeammate?: boolean;
34
+ currentModel?: ClaudeModel | null; // null = not yet detected
27
35
  }
28
36
 
29
37
  // Session list response
@@ -57,6 +65,14 @@ export interface SessionExitEvent {
57
65
  exitCode: number;
58
66
  }
59
67
 
68
+ // Model switch event
69
+ export interface ModelSwitchEvent {
70
+ sessionId: string;
71
+ model: ClaudeModel;
72
+ previousModel: ClaudeModel | null;
73
+ detectedAt: number;
74
+ }
75
+
60
76
  // Persisted session state
61
77
  export interface PersistedSessionState {
62
78
  version: 1;
@@ -124,7 +140,15 @@ export interface LayoutBranch {
124
140
  children: [LayoutNode, LayoutNode];
125
141
  }
126
142
 
127
- export type LayoutNode = LayoutLeaf | LayoutBranch;
143
+ export interface LayoutGrid {
144
+ type: 'grid';
145
+ id: string;
146
+ direction: 'horizontal' | 'vertical'; // row or column
147
+ children: LayoutNode[];
148
+ sizes: number[]; // percentage for each child (must sum to 100)
149
+ }
150
+
151
+ export type LayoutNode = LayoutLeaf | LayoutBranch | LayoutGrid;
128
152
 
129
153
  export interface SplitViewState {
130
154
  layout: LayoutNode;
@@ -147,6 +171,15 @@ export interface AppSettings {
147
171
  sessionPoolSettings?: SessionPoolSettings;
148
172
  autoLayoutTeams?: boolean;
149
173
  atlasSettings?: import('./types/atlas-types').AtlasSettings;
174
+ hasLaunchedBefore?: boolean; // Track first launch for Layout Picker
175
+ lastUsedLayoutPresetId?: string; // Track which preset was last applied
176
+ wizardCompleted?: boolean; // Track if welcome wizard has been completed
177
+ tooltipCoachDismissed?: Record<string, boolean>; // Track dismissed tooltip coach hints
178
+ panelHelpDismissed?: Record<string, boolean>; // Track dismissed panel help overlays
179
+ uiMode?: 'beginner' | 'expert'; // UI complexity mode (default: beginner)
180
+ defaultModel?: ClaudeModel; // Default model for new sessions (default: 'sonnet')
181
+ modelPreset?: ModelPreset; // Model preset mode (default: 'balanced')
182
+ gitSettings?: import('./types/git-types').GitSettings;
150
183
  }
151
184
 
152
185
  // Workspace create request
@@ -323,3 +356,21 @@ export type {
323
356
  AtlasOutputLocation,
324
357
  } from './types/atlas-types';
325
358
 
359
+ export type {
360
+ GitFileStatus,
361
+ GitFileArea,
362
+ GitFileEntry,
363
+ GitBranchInfo,
364
+ GitStatus,
365
+ GitCommitInfo,
366
+ GitDiffResult,
367
+ CommitType,
368
+ CommitConfidence,
369
+ GeneratedCommitMessage,
370
+ GitOperationResult,
371
+ GitErrorCode,
372
+ GitCommitRequest,
373
+ GitRemoteProgress,
374
+ GitSettings,
375
+ } from './types/git-types';
376
+
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { parseMessages, resetParser } from './message-parser';
3
+
4
+ describe('parseMessages', () => {
5
+ beforeEach(() => {
6
+ resetParser();
7
+ });
8
+
9
+ it('parses "@agent> message" format', () => {
10
+ const result = parseMessages('@frontend> Starting the build', 'sess-1');
11
+ expect(result).toHaveLength(1);
12
+ expect(result[0].sender).toBe('frontend');
13
+ expect(result[0].content).toBe('Starting the build');
14
+ expect(result[0].sessionId).toBe('sess-1');
15
+ });
16
+
17
+ it('parses "@agent → @target: message" format', () => {
18
+ const result = parseMessages('@lead → @worker: Do the task', 'sess-1');
19
+ expect(result).toHaveLength(1);
20
+ expect(result[0].sender).toBe('lead');
21
+ expect(result[0].receiver).toBe('worker');
22
+ expect(result[0].content).toBe('Do the task');
23
+ });
24
+
25
+ it('parses "[Agent → Target]: message" format', () => {
26
+ const result = parseMessages('[Lead Agent → Worker]: Start deployment', 'sess-1');
27
+ expect(result).toHaveLength(1);
28
+ expect(result[0].sender).toBe('Lead Agent');
29
+ expect(result[0].receiver).toBe('Worker');
30
+ expect(result[0].content).toBe('Start deployment');
31
+ });
32
+
33
+ it('parses "Sending message to Agent: message" format', () => {
34
+ const result = parseMessages('Sending message to Backend: Update the API', 'sess-1');
35
+ expect(result).toHaveLength(1);
36
+ expect(result[0].sender).toBe('lead');
37
+ expect(result[0].receiver).toBe('Backend');
38
+ expect(result[0].content).toBe('Update the API');
39
+ });
40
+
41
+ it('strips ANSI codes before parsing', () => {
42
+ const ansi = '\x1b[32m@frontend> Building\x1b[0m';
43
+ const result = parseMessages(ansi, 'sess-1');
44
+ expect(result).toHaveLength(1);
45
+ expect(result[0].sender).toBe('frontend');
46
+ expect(result[0].content).toBe('Building');
47
+ });
48
+
49
+ it('deduplicates identical messages', () => {
50
+ const text = '@agent> Hello\n@agent> Hello';
51
+ const result = parseMessages(text, 'sess-1');
52
+ expect(result).toHaveLength(1);
53
+ });
54
+
55
+ it('skips empty lines', () => {
56
+ const result = parseMessages('\n\n\n', 'sess-1');
57
+ expect(result).toHaveLength(0);
58
+ });
59
+
60
+ it('returns empty array for non-matching text', () => {
61
+ const result = parseMessages('This is normal output', 'sess-1');
62
+ expect(result).toHaveLength(0);
63
+ });
64
+
65
+ it('handles multiple different messages', () => {
66
+ const text = '@frontend> Building\n@backend> Ready';
67
+ const result = parseMessages(text, 'sess-1');
68
+ expect(result).toHaveLength(2);
69
+ expect(result[0].sender).toBe('frontend');
70
+ expect(result[1].sender).toBe('backend');
71
+ });
72
+
73
+ it('assigns incremental IDs', () => {
74
+ const result = parseMessages('@a> Hello\n@b> World', 'sess-1');
75
+ expect(result[0].id).toMatch(/^msg-\d+$/);
76
+ expect(result[1].id).toMatch(/^msg-\d+$/);
77
+ expect(result[0].id).not.toBe(result[1].id);
78
+ });
79
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectModelFromOutput } from './model-detector';
3
+
4
+ describe('detectModelFromOutput', () => {
5
+ describe('initial detection (welcome screen)', () => {
6
+ it('detects "Haiku 4.5 ·" format', () => {
7
+ const result = detectModelFromOutput('Haiku 4.5 · Claude Max', true);
8
+ expect(result).toEqual({ model: 'haiku', confidence: 'high' });
9
+ });
10
+
11
+ it('detects "Opus 4.6 ·" format', () => {
12
+ const result = detectModelFromOutput('Opus 4.6 · Claude Max', true);
13
+ expect(result).toEqual({ model: 'opus', confidence: 'high' });
14
+ });
15
+
16
+ it('detects "Sonnet 4.5 ·" format', () => {
17
+ const result = detectModelFromOutput('Sonnet 4.5 · Claude Max', true);
18
+ expect(result).toEqual({ model: 'sonnet', confidence: 'high' });
19
+ });
20
+
21
+ it('detects "Claude 3.5 Sonnet" format', () => {
22
+ const result = detectModelFromOutput('Welcome to Claude 3.5 Sonnet', true);
23
+ expect(result).toEqual({ model: 'sonnet', confidence: 'high' });
24
+ });
25
+
26
+ it('detects "(Opus 4.6 · Most capable" format', () => {
27
+ const result = detectModelFromOutput('(Opus 4.6 · Most capable model)', true);
28
+ expect(result).toEqual({ model: 'opus', confidence: 'high' });
29
+ });
30
+
31
+ it('rejects promo text without ·', () => {
32
+ // "Opus 4.6 is here" should NOT match the first pattern
33
+ // because "is here" appears before "·"
34
+ const result = detectModelFromOutput('Opus 4.6 is here · $50 free', true);
35
+ // This should still match via the second pattern (Claude ... Opus)
36
+ // or not match the first pattern. The key is it doesn't falsely detect.
37
+ // Actually the first pattern requires · immediately after version, "is here" breaks it.
38
+ expect(result.model).toBeNull();
39
+ });
40
+
41
+ it('returns null for unrecognized text', () => {
42
+ const result = detectModelFromOutput('Welcome to the chat', true);
43
+ expect(result).toEqual({ model: null, confidence: 'low' });
44
+ });
45
+
46
+ it('strips ANSI codes before matching', () => {
47
+ const ansi = '\x1b[1m\x1b[34mHaiku 4.5 · Claude Max\x1b[0m';
48
+ const result = detectModelFromOutput(ansi, true);
49
+ expect(result).toEqual({ model: 'haiku', confidence: 'high' });
50
+ });
51
+ });
52
+
53
+ describe('switch detection', () => {
54
+ it('detects "Set model to Opus"', () => {
55
+ const result = detectModelFromOutput('Set model to Opus (claude-opus-4-6)', false);
56
+ expect(result).toEqual({ model: 'opus', confidence: 'high' });
57
+ });
58
+
59
+ it('detects "Set model to Default (Opus 4.6..."', () => {
60
+ const result = detectModelFromOutput('Set model to Default (Opus 4.6 · Most capable)', false);
61
+ expect(result).toEqual({ model: 'opus', confidence: 'high' });
62
+ });
63
+
64
+ it('detects "Kept model as Sonnet"', () => {
65
+ const result = detectModelFromOutput('Kept model as Sonnet', false);
66
+ expect(result).toEqual({ model: 'sonnet', confidence: 'high' });
67
+ });
68
+
69
+ it('detects "Kept model as Default (Haiku"', () => {
70
+ const result = detectModelFromOutput('Kept model as Default (Haiku 4.5)', false);
71
+ expect(result).toEqual({ model: 'haiku', confidence: 'high' });
72
+ });
73
+
74
+ it('detects "Model changed to haiku"', () => {
75
+ const result = detectModelFromOutput('Model changed to haiku', false);
76
+ expect(result).toEqual({ model: 'haiku', confidence: 'high' });
77
+ });
78
+
79
+ it('returns null for unrelated text', () => {
80
+ const result = detectModelFromOutput('This is just some output text', false);
81
+ expect(result).toEqual({ model: null, confidence: 'low' });
82
+ });
83
+
84
+ it('strips ANSI before switch detection', () => {
85
+ const ansi = '\x1b[32mSet model to Opus\x1b[0m';
86
+ const result = detectModelFromOutput(ansi, false);
87
+ expect(result).toEqual({ model: 'opus', confidence: 'high' });
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Model Detector — Parse terminal output to detect active Claude model
3
+ *
4
+ * Detects model switches from Claude Code CLI output in two phases:
5
+ * 1. Initial detection: Parse welcome screen for starting model
6
+ * 2. Switch detection: Parse /model command confirmations
7
+ */
8
+
9
+ export type ClaudeModel = 'sonnet' | 'opus' | 'haiku' | 'auto';
10
+
11
+ export interface ModelDetectionResult {
12
+ model: ClaudeModel | null;
13
+ confidence: 'high' | 'medium' | 'low';
14
+ }
15
+
16
+ // Patterns for detecting model switches via /model command
17
+ const SWITCH_PATTERNS = [
18
+ /Set model to (?:Default )?\(?(Opus|Sonnet|Haiku)/i, // "Set model to Default (Opus 4.6..."
19
+ /Set model to (\w+)/i, // "Set model to opus (claude-opus-4-6)"
20
+ /Kept model as (?:Default )?\(?(Opus|Sonnet|Haiku)/i, // "Kept model as Default (recommended)"
21
+ /Kept model as (\w+)/i, // "Kept model as haiku"
22
+ /Switched to (?:claude[- ])?(\w+)/i,
23
+ /Now using (?:model: )?(\w+)/i,
24
+ /Model changed to (\w+)/i,
25
+ /Using model (\w+)/i,
26
+ ];
27
+
28
+ // Patterns for detecting initial model from welcome screen
29
+ // IMPORTANT: Pattern 0 requires "·" immediately after version to exclude promo text
30
+ // like "Opus 4.6 is here · $50 free extra usage" (which has "is here" before "·")
31
+ const WELCOME_PATTERNS = [
32
+ /(Opus|Sonnet|Haiku)\s+\d+\.\d+\s*·/i, // "Haiku 4.5 · Claude Max" (v2.1+ format, requires ·)
33
+ /Claude (?:3\.5 |4\.\d+ )?(Sonnet|Opus|Haiku)/i,
34
+ /\((Opus|Sonnet|Haiku)\s+\d+\.\d+/i, // "(Opus 4.6 · Most capable..."
35
+ /Using model[: ]+(\w+)/i,
36
+ /Model[: ]+(\w+)/i,
37
+ /\((\w+)\s+\d+\.\d+\)/i, // e.g., "(Sonnet 4.5)"
38
+ ];
39
+
40
+ /**
41
+ * Strip ANSI escape sequences from terminal output.
42
+ * Handles CSI sequences (\x1b[...X), OSC sequences (\x1b]...\x07), and single-char escapes.
43
+ */
44
+ function stripAnsi(text: string): string {
45
+ return text
46
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // CSI sequences (colors, cursor, etc.)
47
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences (title, etc.)
48
+ .replace(/\x1b[()][A-Z0-9]/g, '') // Character set selection
49
+ .replace(/\x1b[=>]/g, ''); // Keypad mode
50
+ }
51
+
52
+ /**
53
+ * Detect model from terminal output.
54
+ *
55
+ * @param text - Terminal output to parse (raw PTY output with ANSI codes is OK)
56
+ * @param isInitial - True for welcome screen detection, false for switch detection
57
+ * @returns Detection result with confidence level
58
+ */
59
+ export function detectModelFromOutput(
60
+ text: string,
61
+ isInitial: boolean = false
62
+ ): ModelDetectionResult {
63
+ const clean = stripAnsi(text);
64
+ const patterns = isInitial ? WELCOME_PATTERNS : SWITCH_PATTERNS;
65
+
66
+ for (const pattern of patterns) {
67
+ const match = clean.match(pattern);
68
+ if (match) {
69
+ const raw = match[1].toLowerCase();
70
+ const normalized = normalizeModelName(raw);
71
+ if (normalized) {
72
+ return { model: normalized, confidence: 'high' };
73
+ }
74
+ }
75
+ }
76
+
77
+ return { model: null, confidence: 'low' };
78
+ }
79
+
80
+ /**
81
+ * Normalize raw model name to canonical ClaudeModel type.
82
+ */
83
+ function normalizeModelName(raw: string): ClaudeModel | null {
84
+ const map: Record<string, ClaudeModel> = {
85
+ 'sonnet': 'sonnet',
86
+ '3.5-sonnet': 'sonnet',
87
+ '4-sonnet': 'sonnet',
88
+ 'opus': 'opus',
89
+ '3-opus': 'opus',
90
+ '4-opus': 'opus',
91
+ 'haiku': 'haiku',
92
+ '3-haiku': 'haiku',
93
+ '4-haiku': 'haiku',
94
+ 'auto': 'auto',
95
+ };
96
+ return map[raw] || null;
97
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Command types for enhanced command palette
3
+ */
4
+
5
+ export type CommandCategory = 'sessions' | 'view' | 'templates' | 'panels' | 'settings' | 'help';
6
+
7
+ export interface Command {
8
+ id: string;
9
+ category: CommandCategory;
10
+ title: string;
11
+ description?: string;
12
+ keywords?: string[]; // For better search
13
+ shortcut?: string; // Display keyboard shortcut
14
+ icon?: string; // Icon name or SVG
15
+ action?: string; // IPC method name or special action
16
+ args?: any[]; // Arguments for the action
17
+ }
18
+
19
+ export interface CommandSearchResult extends Command {
20
+ score: number; // Relevance score for sorting
21
+ }
22
+
23
+ export interface CommandRegistryData {
24
+ commands: Command[];
25
+ categories: Record<CommandCategory, { label: string; icon: string }>;
26
+ }