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
@@ -45,6 +45,10 @@ const history_manager_1 = require("./history-manager");
45
45
  const checkpoint_manager_1 = require("./checkpoint-manager");
46
46
  const agent_team_manager_1 = require("./agent-team-manager");
47
47
  const atlas_manager_1 = require("./atlas-manager");
48
+ const layout_presets_manager_1 = require("./layout-presets-manager");
49
+ const command_registry_1 = require("./command-registry");
50
+ const model_history_manager_1 = require("./model-history-manager");
51
+ const git_manager_1 = require("./git-manager");
48
52
  const ipc_handlers_1 = require("./ipc-handlers");
49
53
  // Check for dev mode via environment variable or if dist files don't exist
50
54
  const isDev = process.env.ELECTRON_IS_DEV === 'true' ||
@@ -58,6 +62,10 @@ let historyManager = null;
58
62
  let checkpointManager = null;
59
63
  let agentTeamManager = null;
60
64
  let atlasManager = null;
65
+ let layoutPresetsManager = null;
66
+ let commandRegistry = null;
67
+ let modelHistoryManager = null;
68
+ let gitManager = null;
61
69
  const CONFIG_DIR = path.join(electron_1.app.getPath('home'), '.claudedesk');
62
70
  const WINDOW_STATE_FILE = path.join(CONFIG_DIR, 'window-state.json');
63
71
  function ensureConfigDir() {
@@ -135,9 +143,11 @@ function createWindow() {
135
143
  }
136
144
  // Initialize managers
137
145
  settingsManager = new settings_persistence_1.SettingsManager();
146
+ layoutPresetsManager = new layout_presets_manager_1.LayoutPresetsManager(settingsManager);
138
147
  templatesManager = new prompt_templates_manager_1.PromptTemplatesManager();
139
148
  historyManager = new history_manager_1.HistoryManager();
140
149
  checkpointManager = new checkpoint_manager_1.CheckpointManager(historyManager);
150
+ modelHistoryManager = new model_history_manager_1.ModelHistoryManager();
141
151
  // Create session pool with config
142
152
  const poolSettings = settingsManager.getSessionPoolSettings();
143
153
  sessionPool = new session_pool_1.SessionPool({
@@ -147,6 +157,7 @@ function createWindow() {
147
157
  });
148
158
  // Create session manager with pool reference
149
159
  sessionManager = new session_manager_1.SessionManager(historyManager, sessionPool);
160
+ sessionManager.setModelHistoryManager(modelHistoryManager);
150
161
  sessionManager.initialize();
151
162
  // Validate split view state against current sessions
152
163
  const sessionList = sessionManager.listSessions();
@@ -155,6 +166,11 @@ function createWindow() {
155
166
  // Initialize atlas manager
156
167
  atlasManager = new atlas_manager_1.AtlasManager();
157
168
  atlasManager.setMainWindow(mainWindow);
169
+ // Initialize git manager
170
+ gitManager = new git_manager_1.GitManager(checkpointManager);
171
+ gitManager.setMainWindow(mainWindow);
172
+ // Initialize command registry
173
+ commandRegistry = new command_registry_1.CommandRegistry();
158
174
  // Initialize agent team manager
159
175
  agentTeamManager = new agent_team_manager_1.AgentTeamManager();
160
176
  agentTeamManager.setMainWindow(mainWindow);
@@ -164,7 +180,7 @@ function createWindow() {
164
180
  agentTeamManager?.onSessionClosed(sessionId);
165
181
  });
166
182
  // Setup IPC handlers with pool reference
167
- (0, ipc_handlers_1.setupIPCHandlers)(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager);
183
+ (0, ipc_handlers_1.setupIPCHandlers)(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager);
168
184
  // Initialize pool (delayed, async)
169
185
  setTimeout(() => {
170
186
  if (sessionPool) {
@@ -209,6 +225,14 @@ function createWindow() {
209
225
  });
210
226
  mainWindow.on('closed', () => {
211
227
  (0, ipc_handlers_1.removeIPCHandlers)();
228
+ if (modelHistoryManager) {
229
+ modelHistoryManager.shutdown();
230
+ modelHistoryManager = null;
231
+ }
232
+ if (gitManager) {
233
+ gitManager.destroy();
234
+ gitManager = null;
235
+ }
212
236
  if (atlasManager) {
213
237
  atlasManager.destroy();
214
238
  atlasManager = null;
@@ -42,7 +42,7 @@ const quota_service_1 = require("./quota-service");
42
42
  const file_dragdrop_handler_1 = require("./file-dragdrop-handler");
43
43
  const ipc_registry_1 = require("./ipc-registry");
44
44
  let registry = null;
45
- function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager) {
45
+ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templatesManager, historyManager, checkpointManager, sessionPool, agentTeamManager, atlasManager, layoutPresetsManager, commandRegistry, modelHistoryManager, gitManager) {
46
46
  // Connect managers to window
47
47
  sessionManager.setMainWindow(mainWindow);
48
48
  checkpointManager.setMainWindow(mainWindow);
@@ -50,7 +50,8 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
50
50
  // ── Session management (invoke) ──
51
51
  registry.handle('createSession', async (_e, request) => {
52
52
  try {
53
- return await sessionManager.createSession(request);
53
+ const result = await sessionManager.createSession(request);
54
+ return result;
54
55
  }
55
56
  catch (err) {
56
57
  console.error('Failed to create session:', err);
@@ -65,7 +66,8 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
65
66
  });
66
67
  registry.handle('renameSession', async (_e, sessionId, newName) => {
67
68
  try {
68
- return await sessionManager.renameSession(sessionId, newName);
69
+ const result = await sessionManager.renameSession(sessionId, newName);
70
+ return result;
69
71
  }
70
72
  catch (err) {
71
73
  console.error('Failed to rename session:', err);
@@ -81,6 +83,47 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
81
83
  registry.handle('getActiveSession', async () => {
82
84
  return sessionManager.getActiveSessionId();
83
85
  });
86
+ // ── Model switching ──
87
+ registry.handle('switchModel', async (_e, sessionId, model) => {
88
+ const session = sessionManager.getSession(sessionId);
89
+ console.log('[switchModel] Called with:', { sessionId, model, sessionStatus: session?.status });
90
+ if (!session) {
91
+ console.log('[switchModel] Session not found');
92
+ return false;
93
+ }
94
+ if (session.status === 'exited' || session.status === 'error') {
95
+ console.log('[switchModel] Session not active:', session.status);
96
+ return false;
97
+ }
98
+ const VALID_MODELS = ['opus', 'sonnet', 'haiku'];
99
+ if (!VALID_MODELS.includes(model)) {
100
+ console.log('[switchModel] Unknown model:', model);
101
+ return false;
102
+ }
103
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
104
+ // Use direct command syntax: /model haiku, /model sonnet, /model opus
105
+ // Type character-by-character to avoid autocomplete interference
106
+ const command = `/model ${model}`;
107
+ console.log('[switchModel] Typing command:', command);
108
+ for (const char of command) {
109
+ sessionManager.sendInput(sessionId, char);
110
+ await delay(50);
111
+ }
112
+ // Wait for autocomplete to settle
113
+ await delay(300);
114
+ // Press Enter to execute
115
+ sessionManager.sendInput(sessionId, '\r');
116
+ console.log('[switchModel] Command sent');
117
+ return true;
118
+ });
119
+ // ── Model History ──
120
+ registry.handle('getModelHistory', async (_e, sessionId) => {
121
+ return modelHistoryManager.getHistory(sessionId);
122
+ });
123
+ registry.handle('clearModelHistory', async (_e, sessionId) => {
124
+ modelHistoryManager.clearHistory(sessionId);
125
+ return true;
126
+ });
84
127
  // ── Dialogs ──
85
128
  registry.handle('browseDirectory', async () => {
86
129
  const result = await electron_1.dialog.showOpenDialog(mainWindow, {
@@ -239,6 +282,43 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
239
282
  throw err;
240
283
  }
241
284
  });
285
+ // ── Commands ──
286
+ registry.handle('searchCommands', async (_e, query, maxResults = 10) => {
287
+ try {
288
+ return commandRegistry.search(query, maxResults);
289
+ }
290
+ catch (err) {
291
+ console.error('Failed to search commands:', err);
292
+ throw err;
293
+ }
294
+ });
295
+ registry.handle('getAllCommands', async () => {
296
+ try {
297
+ return commandRegistry.getAllCommands();
298
+ }
299
+ catch (err) {
300
+ console.error('Failed to get all commands:', err);
301
+ throw err;
302
+ }
303
+ });
304
+ registry.handle('executeCommand', async (_e, commandId, args) => {
305
+ try {
306
+ const command = commandRegistry.getCommand(commandId);
307
+ if (!command) {
308
+ console.error('Command not found:', commandId);
309
+ return false;
310
+ }
311
+ // Execute command action
312
+ // For now, commands with UI actions will be handled by the renderer
313
+ // This is a placeholder for future server-side command execution
314
+ console.log('Executing command:', commandId, args);
315
+ return true;
316
+ }
317
+ catch (err) {
318
+ console.error('Failed to execute command:', err);
319
+ return false;
320
+ }
321
+ });
242
322
  // ── Drag-Drop ──
243
323
  registry.handle('getFileInfo', async (_e, filePaths) => {
244
324
  try {
@@ -438,6 +518,26 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
438
518
  return false;
439
519
  }
440
520
  });
521
+ registry.handle('updateUIMode', async (_e, mode) => {
522
+ try {
523
+ settingsManager.updateUIMode(mode);
524
+ return true;
525
+ }
526
+ catch (err) {
527
+ console.error('Failed to update UI mode:', err);
528
+ return false;
529
+ }
530
+ });
531
+ registry.handle('updateDefaultModel', async (_e, model) => {
532
+ try {
533
+ settingsManager.updateDefaultModel(model);
534
+ return true;
535
+ }
536
+ catch (err) {
537
+ console.error('Failed to update default model:', err);
538
+ return false;
539
+ }
540
+ });
441
541
  // ── Repository Atlas ──
442
542
  registry.handle('generateAtlas', async (_e, request) => {
443
543
  try {
@@ -476,6 +576,250 @@ function setupIPCHandlers(mainWindow, sessionManager, settingsManager, templates
476
576
  throw err;
477
577
  }
478
578
  });
579
+ // ── Layout Presets ──
580
+ registry.handle('getLayoutPresets', async () => {
581
+ return layoutPresetsManager.getPresets();
582
+ });
583
+ registry.handle('applyLayoutPreset', async (_e, presetId) => {
584
+ try {
585
+ const preset = layoutPresetsManager.getPresetById(presetId);
586
+ if (!preset) {
587
+ console.error('Preset not found:', presetId);
588
+ return false;
589
+ }
590
+ // Validate the preset structure
591
+ if (!layoutPresetsManager.validateLayout(preset.structure)) {
592
+ console.error('Invalid preset structure:', presetId);
593
+ return false;
594
+ }
595
+ // Update split view state with preset layout
596
+ settingsManager.updateSplitViewState({
597
+ layout: preset.structure,
598
+ focusedPaneId: '', // Will be set by renderer
599
+ });
600
+ // Save the last used preset ID
601
+ layoutPresetsManager.saveLastUsedPreset(presetId);
602
+ return true;
603
+ }
604
+ catch (err) {
605
+ console.error('Failed to apply layout preset:', err);
606
+ return false;
607
+ }
608
+ });
609
+ registry.handle('applyCustomLayout', async (_e, rows, cols) => {
610
+ try {
611
+ const customLayout = layoutPresetsManager.createCustomGridLayout(rows, cols);
612
+ // Validate the generated layout
613
+ if (!layoutPresetsManager.validateLayout(customLayout)) {
614
+ console.error('Invalid custom layout generated');
615
+ return false;
616
+ }
617
+ // Update split view state
618
+ settingsManager.updateSplitViewState({
619
+ layout: customLayout,
620
+ focusedPaneId: '', // Will be set by renderer
621
+ });
622
+ // Save as custom preset
623
+ layoutPresetsManager.saveLastUsedPreset('custom');
624
+ return true;
625
+ }
626
+ catch (err) {
627
+ console.error('Failed to apply custom layout:', err);
628
+ return false;
629
+ }
630
+ });
631
+ registry.handle('getCurrentLayout', async () => {
632
+ const settings = settingsManager.getSettings();
633
+ if (settings.splitViewState) {
634
+ return settings.splitViewState.layout;
635
+ }
636
+ // Return default single pane layout
637
+ const { v4: uuidv4 } = require('uuid');
638
+ const defaultLayout = {
639
+ type: 'leaf',
640
+ paneId: uuidv4(),
641
+ sessionId: null,
642
+ };
643
+ return defaultLayout;
644
+ });
645
+ // ── Git Integration ──
646
+ registry.handle('getGitStatus', async (_e, workDir) => {
647
+ try {
648
+ return await gitManager.getStatus(workDir);
649
+ }
650
+ catch (err) {
651
+ console.error('Failed to get git status:', err);
652
+ throw err;
653
+ }
654
+ });
655
+ registry.handle('getGitBranches', async (_e, workDir) => {
656
+ try {
657
+ return await gitManager.getBranches(workDir);
658
+ }
659
+ catch (err) {
660
+ console.error('Failed to get git branches:', err);
661
+ throw err;
662
+ }
663
+ });
664
+ registry.handle('gitStageFiles', async (_e, workDir, files) => {
665
+ try {
666
+ return await gitManager.stageFiles(workDir, files);
667
+ }
668
+ catch (err) {
669
+ console.error('Failed to stage files:', err);
670
+ throw err;
671
+ }
672
+ });
673
+ registry.handle('gitUnstageFiles', async (_e, workDir, files) => {
674
+ try {
675
+ return await gitManager.unstageFiles(workDir, files);
676
+ }
677
+ catch (err) {
678
+ console.error('Failed to unstage files:', err);
679
+ throw err;
680
+ }
681
+ });
682
+ registry.handle('gitStageAll', async (_e, workDir) => {
683
+ try {
684
+ return await gitManager.stageAll(workDir);
685
+ }
686
+ catch (err) {
687
+ console.error('Failed to stage all:', err);
688
+ throw err;
689
+ }
690
+ });
691
+ registry.handle('gitUnstageAll', async (_e, workDir) => {
692
+ try {
693
+ return await gitManager.unstageAll(workDir);
694
+ }
695
+ catch (err) {
696
+ console.error('Failed to unstage all:', err);
697
+ throw err;
698
+ }
699
+ });
700
+ registry.handle('gitCommit', async (_e, request) => {
701
+ try {
702
+ return await gitManager.commit(request);
703
+ }
704
+ catch (err) {
705
+ console.error('Failed to commit:', err);
706
+ throw err;
707
+ }
708
+ });
709
+ registry.handle('gitGenerateMessage', async (_e, workDir) => {
710
+ try {
711
+ return await gitManager.generateMessage(workDir);
712
+ }
713
+ catch (err) {
714
+ console.error('Failed to generate commit message:', err);
715
+ throw err;
716
+ }
717
+ });
718
+ registry.handle('gitPush', async (_e, workDir, setUpstream) => {
719
+ try {
720
+ return await gitManager.push(workDir, setUpstream);
721
+ }
722
+ catch (err) {
723
+ console.error('Failed to push:', err);
724
+ throw err;
725
+ }
726
+ });
727
+ registry.handle('gitPull', async (_e, workDir) => {
728
+ try {
729
+ return await gitManager.pull(workDir);
730
+ }
731
+ catch (err) {
732
+ console.error('Failed to pull:', err);
733
+ throw err;
734
+ }
735
+ });
736
+ registry.handle('gitFetch', async (_e, workDir) => {
737
+ try {
738
+ return await gitManager.fetch(workDir);
739
+ }
740
+ catch (err) {
741
+ console.error('Failed to fetch:', err);
742
+ throw err;
743
+ }
744
+ });
745
+ registry.handle('gitSwitchBranch', async (_e, workDir, branch) => {
746
+ try {
747
+ return await gitManager.switchBranch(workDir, branch);
748
+ }
749
+ catch (err) {
750
+ console.error('Failed to switch branch:', err);
751
+ throw err;
752
+ }
753
+ });
754
+ registry.handle('gitCreateBranch', async (_e, workDir, branch) => {
755
+ try {
756
+ return await gitManager.createBranch(workDir, branch);
757
+ }
758
+ catch (err) {
759
+ console.error('Failed to create branch:', err);
760
+ throw err;
761
+ }
762
+ });
763
+ registry.handle('gitLog', async (_e, workDir, count) => {
764
+ try {
765
+ return await gitManager.log(workDir, count);
766
+ }
767
+ catch (err) {
768
+ console.error('Failed to get git log:', err);
769
+ throw err;
770
+ }
771
+ });
772
+ registry.handle('gitDiff', async (_e, workDir, filePath, staged) => {
773
+ try {
774
+ return await gitManager.diff(workDir, filePath, staged);
775
+ }
776
+ catch (err) {
777
+ console.error('Failed to get diff:', err);
778
+ throw err;
779
+ }
780
+ });
781
+ registry.handle('gitCommitDiff', async (_e, workDir, hash) => {
782
+ try {
783
+ return await gitManager.commitDiff(workDir, hash);
784
+ }
785
+ catch (err) {
786
+ console.error('Failed to get commit diff:', err);
787
+ throw err;
788
+ }
789
+ });
790
+ registry.handle('gitDiscardFile', async (_e, workDir, filePath) => {
791
+ try {
792
+ return await gitManager.discardFile(workDir, filePath);
793
+ }
794
+ catch (err) {
795
+ console.error('Failed to discard file:', err);
796
+ throw err;
797
+ }
798
+ });
799
+ registry.handle('gitDiscardAll', async (_e, workDir) => {
800
+ try {
801
+ return await gitManager.discardAll(workDir);
802
+ }
803
+ catch (err) {
804
+ console.error('Failed to discard all:', err);
805
+ throw err;
806
+ }
807
+ });
808
+ registry.handle('gitInit', async (_e, workDir) => {
809
+ try {
810
+ return await gitManager.init(workDir);
811
+ }
812
+ catch (err) {
813
+ console.error('Failed to init git:', err);
814
+ throw err;
815
+ }
816
+ });
817
+ registry.handle('gitStartWatching', async (_e, workDir) => {
818
+ return gitManager.startWatching(workDir);
819
+ });
820
+ registry.handle('gitStopWatching', async (_e, workDir) => {
821
+ return gitManager.stopWatching(workDir);
822
+ });
479
823
  // ── App info ──
480
824
  registry.handle('getVersionInfo', async () => {
481
825
  const { app } = require('electron');
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LayoutPresetsManager = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const layout_presets_1 = require("../types/layout-presets");
6
+ /**
7
+ * LayoutPresetsManager — Manages layout presets, custom grid generation, and validation.
8
+ */
9
+ class LayoutPresetsManager {
10
+ constructor(settingsPersistence) {
11
+ this.settingsPersistence = settingsPersistence;
12
+ this.defaultPresets = this.buildDefaultPresets();
13
+ }
14
+ /**
15
+ * Get all available layout presets
16
+ */
17
+ getPresets() {
18
+ return this.defaultPresets;
19
+ }
20
+ /**
21
+ * Get a specific preset by ID
22
+ */
23
+ getPresetById(id) {
24
+ return this.defaultPresets.find(p => p.id === id) || null;
25
+ }
26
+ /**
27
+ * Generate a custom grid layout from rows and columns
28
+ */
29
+ createCustomGridLayout(rows, cols) {
30
+ // Validate inputs
31
+ if (rows < 1 || rows > 6 || cols < 1 || cols > 6) {
32
+ throw new Error('Rows and columns must be between 1 and 6');
33
+ }
34
+ // If single pane, return a leaf
35
+ if (rows === 1 && cols === 1) {
36
+ return this.createLeaf();
37
+ }
38
+ // If single row, create horizontal grid
39
+ if (rows === 1) {
40
+ return this.createHorizontalGrid(cols);
41
+ }
42
+ // If single column, create vertical grid
43
+ if (cols === 1) {
44
+ return this.createVerticalGrid(rows);
45
+ }
46
+ // Otherwise, create a grid of rows, each containing columns
47
+ const rowSize = 100 / rows;
48
+ const colSize = 100 / cols;
49
+ const rowChildren = [];
50
+ for (let r = 0; r < rows; r++) {
51
+ const colChildren = [];
52
+ for (let c = 0; c < cols; c++) {
53
+ colChildren.push(this.createLeaf());
54
+ }
55
+ // Create horizontal grid for this row
56
+ rowChildren.push({
57
+ type: 'grid',
58
+ id: (0, uuid_1.v4)(),
59
+ direction: 'horizontal',
60
+ children: colChildren,
61
+ sizes: new Array(cols).fill(colSize),
62
+ });
63
+ }
64
+ // Return vertical grid of rows
65
+ return {
66
+ type: 'grid',
67
+ id: (0, uuid_1.v4)(),
68
+ direction: 'vertical',
69
+ children: rowChildren,
70
+ sizes: new Array(rows).fill(rowSize),
71
+ };
72
+ }
73
+ /**
74
+ * Validate a layout structure
75
+ */
76
+ validateLayout(node) {
77
+ try {
78
+ return this.validateNode(node);
79
+ }
80
+ catch (err) {
81
+ console.error('Layout validation failed:', err);
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Save the last used preset ID to settings
87
+ */
88
+ saveLastUsedPreset(presetId) {
89
+ try {
90
+ this.settingsPersistence.updateLastUsedLayoutPreset(presetId);
91
+ }
92
+ catch (err) {
93
+ console.error('Failed to save last used preset:', err);
94
+ }
95
+ }
96
+ // ── Private helpers ──
97
+ buildDefaultPresets() {
98
+ return [
99
+ {
100
+ id: layout_presets_1.PRESET_IDS.SINGLE,
101
+ name: 'Single Pane',
102
+ description: 'One full-width terminal',
103
+ shortcut: '1',
104
+ structure: this.createLeaf(),
105
+ },
106
+ {
107
+ id: layout_presets_1.PRESET_IDS.TWO_COLUMN,
108
+ name: '2-Column Layout',
109
+ description: 'Two terminals side-by-side',
110
+ shortcut: '2',
111
+ structure: this.createHorizontalGrid(2),
112
+ },
113
+ {
114
+ id: layout_presets_1.PRESET_IDS.THREE_COLUMN,
115
+ name: '3-Column Layout',
116
+ description: 'Three equal-width terminals',
117
+ shortcut: '3',
118
+ structure: this.createHorizontalGrid(3),
119
+ },
120
+ {
121
+ id: layout_presets_1.PRESET_IDS.QUAD_GRID,
122
+ name: '2×2 Grid',
123
+ description: 'Four terminals in a grid',
124
+ shortcut: '4',
125
+ structure: this.createQuadGrid(),
126
+ },
127
+ {
128
+ id: layout_presets_1.PRESET_IDS.CUSTOM,
129
+ name: 'Custom Layout',
130
+ description: 'Build your own grid',
131
+ shortcut: '5',
132
+ structure: this.createLeaf(), // Placeholder, overridden by builder
133
+ isCustom: true,
134
+ },
135
+ ];
136
+ }
137
+ createLeaf() {
138
+ return {
139
+ type: 'leaf',
140
+ paneId: (0, uuid_1.v4)(),
141
+ sessionId: null,
142
+ };
143
+ }
144
+ createHorizontalGrid(count) {
145
+ const size = 100 / count;
146
+ const children = [];
147
+ for (let i = 0; i < count; i++) {
148
+ children.push(this.createLeaf());
149
+ }
150
+ return {
151
+ type: 'grid',
152
+ id: (0, uuid_1.v4)(),
153
+ direction: 'horizontal',
154
+ children,
155
+ sizes: new Array(count).fill(size),
156
+ };
157
+ }
158
+ createVerticalGrid(count) {
159
+ const size = 100 / count;
160
+ const children = [];
161
+ for (let i = 0; i < count; i++) {
162
+ children.push(this.createLeaf());
163
+ }
164
+ return {
165
+ type: 'grid',
166
+ id: (0, uuid_1.v4)(),
167
+ direction: 'vertical',
168
+ children,
169
+ sizes: new Array(count).fill(size),
170
+ };
171
+ }
172
+ createQuadGrid() {
173
+ // 2×2 grid: vertical split of two horizontal splits
174
+ const topRow = {
175
+ type: 'grid',
176
+ id: (0, uuid_1.v4)(),
177
+ direction: 'horizontal',
178
+ children: [this.createLeaf(), this.createLeaf()],
179
+ sizes: [50, 50],
180
+ };
181
+ const bottomRow = {
182
+ type: 'grid',
183
+ id: (0, uuid_1.v4)(),
184
+ direction: 'horizontal',
185
+ children: [this.createLeaf(), this.createLeaf()],
186
+ sizes: [50, 50],
187
+ };
188
+ return {
189
+ type: 'grid',
190
+ id: (0, uuid_1.v4)(),
191
+ direction: 'vertical',
192
+ children: [topRow, bottomRow],
193
+ sizes: [50, 50],
194
+ };
195
+ }
196
+ validateNode(node) {
197
+ if (!node || typeof node !== 'object') {
198
+ return false;
199
+ }
200
+ if (node.type === 'leaf') {
201
+ return typeof node.paneId === 'string';
202
+ }
203
+ if (node.type === 'branch') {
204
+ return (typeof node.direction === 'string' &&
205
+ typeof node.ratio === 'number' &&
206
+ node.ratio >= 0 &&
207
+ node.ratio <= 1 &&
208
+ Array.isArray(node.children) &&
209
+ node.children.length === 2 &&
210
+ this.validateNode(node.children[0]) &&
211
+ this.validateNode(node.children[1]));
212
+ }
213
+ if (node.type === 'grid') {
214
+ if (typeof node.id !== 'string' ||
215
+ (node.direction !== 'horizontal' && node.direction !== 'vertical') ||
216
+ !Array.isArray(node.children) ||
217
+ !Array.isArray(node.sizes) ||
218
+ node.children.length !== node.sizes.length ||
219
+ node.children.length === 0) {
220
+ return false;
221
+ }
222
+ // Validate sizes sum to approximately 100
223
+ const sum = node.sizes.reduce((a, b) => a + b, 0);
224
+ if (Math.abs(sum - 100) > 0.5) {
225
+ return false;
226
+ }
227
+ // Validate all children
228
+ return node.children.every(child => this.validateNode(child));
229
+ }
230
+ return false;
231
+ }
232
+ }
233
+ exports.LayoutPresetsManager = LayoutPresetsManager;