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.
- package/.github/workflows/ci.yml +44 -2
- package/CLAUDE.md +36 -3
- package/PHASE_1_IMPLEMENTATION.md +313 -0
- package/PHASE_2_PARTIAL_IMPLEMENTATION.md +286 -0
- package/dist/main/cli-manager.js +67 -2
- package/dist/main/command-registry.js +196 -0
- package/dist/main/git-manager.js +841 -0
- package/dist/main/index.js +25 -1
- package/dist/main/ipc-handlers.js +347 -3
- package/dist/main/layout-presets-manager.js +233 -0
- package/dist/main/model-history-manager.js +187 -0
- package/dist/main/session-manager.js +83 -26
- package/dist/main/session-persistence.js +1 -0
- package/dist/main/session-pool.js +40 -9
- package/dist/main/settings-persistence.js +67 -12
- package/dist/renderer/assets/index-BNeYLqV4.css +32 -0
- package/dist/renderer/assets/index-D5O5Ljoo.js +17189 -0
- package/dist/renderer/index.html +2 -2
- package/dist/shared/ipc-contract.js +79 -0
- package/dist/shared/model-detector.js +83 -0
- package/dist/shared/types/command-types.js +5 -0
- package/dist/shared/types/git-types.js +2 -0
- package/dist/types/layout-presets.js +11 -0
- package/docs/git-integration-implementation-tasks.md +974 -0
- package/docs/git-integration-product-spec.md +916 -0
- package/docs/git-integration-ui-spec.md +1464 -0
- package/docs/repo-index.md +83 -8
- package/e2e/app-launch.spec.ts +31 -0
- package/e2e/fixtures/electron.ts +34 -0
- package/e2e/keyboard-shortcuts.spec.ts +50 -0
- package/e2e/session.spec.ts +34 -0
- package/e2e/split-view.spec.ts +21 -0
- package/package.json +16 -3
- package/playwright.config.ts +15 -0
- package/src/main/cli-manager.ts +74 -3
- package/src/main/command-registry.ts +221 -0
- package/src/main/git-manager.test.ts +374 -0
- package/src/main/git-manager.ts +909 -0
- package/src/main/index.ts +31 -1
- package/src/main/ipc-emitter.test.ts +60 -0
- package/src/main/ipc-handlers.ts +295 -3
- package/src/main/ipc-registry.test.ts +75 -0
- package/src/main/layout-presets-manager.ts +268 -0
- package/src/main/model-history-manager.ts +196 -0
- package/src/main/session-manager.ts +102 -30
- package/src/main/session-persistence.test.ts +215 -0
- package/src/main/session-persistence.ts +1 -0
- package/src/main/session-pool.ts +31 -9
- package/src/main/settings-persistence.ts +74 -12
- package/src/renderer/App.tsx +215 -43
- package/src/renderer/components/CustomLayoutBuilder.tsx +143 -0
- package/src/renderer/components/GitPanel.test.tsx +181 -0
- package/src/renderer/components/GitPanel.tsx +1407 -0
- package/src/renderer/components/LayoutPicker.tsx +182 -0
- package/src/renderer/components/LayoutPreviewCard.tsx +175 -0
- package/src/renderer/components/ModelHistoryPanel.tsx +435 -0
- package/src/renderer/components/PaneHeader.test.tsx +96 -0
- package/src/renderer/components/PaneHeader.tsx +28 -0
- package/src/renderer/components/SplitLayout.test.tsx +153 -0
- package/src/renderer/components/SplitLayout.tsx +36 -1
- package/src/renderer/components/Terminal.tsx +10 -10
- package/src/renderer/components/WelcomeWizard.tsx +143 -0
- package/src/renderer/components/WizardStepper.tsx +135 -0
- package/src/renderer/components/ui/ClaudeReadinessProgress.tsx +168 -0
- package/src/renderer/components/ui/CommitDialog.test.tsx +134 -0
- package/src/renderer/components/ui/CommitDialog.tsx +464 -0
- package/src/renderer/components/ui/EmptyState.test.tsx +87 -0
- package/src/renderer/components/ui/EmptyState.tsx +115 -86
- package/src/renderer/components/ui/FeatureShowcase.tsx +187 -0
- package/src/renderer/components/ui/FuelGaugeBar.tsx +59 -0
- package/src/renderer/components/ui/FuelStatusIndicator.tsx +358 -0
- package/src/renderer/components/ui/FuelTooltip.tsx +267 -0
- package/src/renderer/components/ui/HelpButton.tsx +43 -0
- package/src/renderer/components/ui/ModelBadge.tsx +72 -0
- package/src/renderer/components/ui/ModelSwitcher.tsx +180 -0
- package/src/renderer/components/ui/PanelFooter.tsx +90 -0
- package/src/renderer/components/ui/PanelHeader.tsx +87 -0
- package/src/renderer/components/ui/PanelHelpOverlay.tsx +274 -0
- package/src/renderer/components/ui/QuickActionCard.tsx +103 -0
- package/src/renderer/components/ui/RecentSessionsList.tsx +154 -0
- package/src/renderer/components/ui/SessionStatusIndicator.tsx +104 -0
- package/src/renderer/components/ui/SettingsDialog.tsx +94 -0
- package/src/renderer/components/ui/ShortcutsPanel.tsx +433 -0
- package/src/renderer/components/ui/StatusPopover.tsx +344 -0
- package/src/renderer/components/ui/TabBar.test.tsx +124 -0
- package/src/renderer/components/ui/TabBar.tsx +152 -168
- package/src/renderer/components/ui/ToolbarDropdown.tsx +227 -0
- package/src/renderer/components/ui/ToolsDropdown.tsx +119 -0
- package/src/renderer/components/ui/TooltipCoach.tsx +217 -0
- package/src/renderer/components/ui/WelcomeHero.tsx +85 -0
- package/src/renderer/components/ui/index.ts +5 -0
- package/src/renderer/components/wizard/Step1_Welcome.tsx +166 -0
- package/src/renderer/components/wizard/Step2_LayoutPicker.tsx +246 -0
- package/src/renderer/components/wizard/Step3_Features.tsx +278 -0
- package/src/renderer/components/wizard/Step4_Ready.tsx +279 -0
- package/src/renderer/hooks/useGit.test.ts +140 -0
- package/src/renderer/hooks/useGit.ts +395 -0
- package/src/renderer/hooks/useLayoutPicker.ts +77 -0
- package/src/renderer/hooks/useModelHistory.ts +69 -0
- package/src/renderer/hooks/useSessionManager.test.ts +146 -0
- package/src/renderer/hooks/useSessionManager.ts +5 -0
- package/src/renderer/hooks/useSplitView.test.ts +168 -0
- package/src/renderer/hooks/useSplitView.ts +126 -128
- package/src/renderer/styles/globals.css +505 -0
- package/src/renderer/utils/fuzzy-search.test.ts +121 -0
- package/src/renderer/utils/layout-tree.test.ts +310 -0
- package/src/renderer/utils/layout-tree.ts +170 -0
- package/src/renderer/utils/variable-resolver.test.ts +102 -0
- package/src/shared/ipc-contract.ts +157 -0
- package/src/shared/ipc-types.ts +52 -1
- package/src/shared/message-parser.test.ts +79 -0
- package/src/shared/model-detector.test.ts +90 -0
- package/src/shared/model-detector.ts +97 -0
- package/src/shared/types/command-types.ts +26 -0
- package/src/shared/types/git-types.ts +126 -0
- package/src/types/layout-presets.ts +22 -0
- package/test/helpers/electron-api-mock.ts +52 -0
- package/test/setup-main.ts +61 -0
- package/test/setup-renderer.ts +8 -0
- package/tsconfig.json +1 -0
- package/tsconfig.main.json +2 -1
- package/vitest.workspace.ts +37 -0
- package/dist/renderer/assets/index-CR22a7j2.css +0 -32
- 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
|
|
package/src/shared/ipc-types.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|