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
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ModelHistoryManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const electron_1 = require("electron");
40
+ const uuid_1 = require("uuid");
41
+ class ModelHistoryManager {
42
+ constructor() {
43
+ this.pendingWrite = null;
44
+ this.lastSwitchBySession = new Map();
45
+ const userDataPath = electron_1.app.getPath('userData');
46
+ const historyDir = path.join(userDataPath, 'model-history');
47
+ // Ensure directory exists
48
+ if (!fs.existsSync(historyDir)) {
49
+ fs.mkdirSync(historyDir, { recursive: true });
50
+ }
51
+ this.indexPath = path.join(historyDir, 'index.json');
52
+ this.index = this.loadIndex();
53
+ }
54
+ loadIndex() {
55
+ try {
56
+ if (fs.existsSync(this.indexPath)) {
57
+ const data = fs.readFileSync(this.indexPath, 'utf-8');
58
+ const loaded = JSON.parse(data);
59
+ // Validate structure
60
+ if (loaded.version === 1 && loaded.entries && loaded.bySession) {
61
+ return loaded;
62
+ }
63
+ }
64
+ }
65
+ catch (err) {
66
+ console.error('[ModelHistoryManager] Failed to load index:', err);
67
+ }
68
+ // Return empty index if file doesn't exist or is invalid
69
+ return {
70
+ version: 1,
71
+ entries: {},
72
+ bySession: {},
73
+ };
74
+ }
75
+ scheduleWrite() {
76
+ if (this.pendingWrite) {
77
+ clearTimeout(this.pendingWrite);
78
+ }
79
+ // Debounce writes (1 second)
80
+ this.pendingWrite = setTimeout(() => {
81
+ this.flushToDisk();
82
+ this.pendingWrite = null;
83
+ }, 1000);
84
+ }
85
+ flushToDisk() {
86
+ try {
87
+ const tmpPath = `${this.indexPath}.tmp`;
88
+ const data = JSON.stringify(this.index, null, 2);
89
+ // Atomic write: write to temp file, then rename
90
+ fs.writeFileSync(tmpPath, data, 'utf-8');
91
+ fs.renameSync(tmpPath, this.indexPath);
92
+ }
93
+ catch (err) {
94
+ console.error('[ModelHistoryManager] Failed to write index:', err);
95
+ }
96
+ }
97
+ /**
98
+ * Log a model switch event
99
+ */
100
+ logSwitch(event) {
101
+ const { sessionId, model, previousModel, detectedAt } = event;
102
+ // Calculate duration if we have a previous switch for this session
103
+ let durationMs;
104
+ const lastSwitch = this.lastSwitchBySession.get(sessionId);
105
+ if (lastSwitch) {
106
+ durationMs = detectedAt - lastSwitch.timestamp;
107
+ }
108
+ // Create entry
109
+ const entry = {
110
+ id: (0, uuid_1.v4)(),
111
+ sessionId,
112
+ timestamp: detectedAt,
113
+ fromModel: previousModel,
114
+ toModel: model,
115
+ durationMs,
116
+ };
117
+ // Add to index
118
+ this.index.entries[entry.id] = entry;
119
+ // Add to session's chronological list
120
+ if (!this.index.bySession[sessionId]) {
121
+ this.index.bySession[sessionId] = [];
122
+ }
123
+ this.index.bySession[sessionId].push(entry.id);
124
+ // Update last switch tracker
125
+ this.lastSwitchBySession.set(sessionId, {
126
+ timestamp: detectedAt,
127
+ model,
128
+ });
129
+ // Schedule write
130
+ this.scheduleWrite();
131
+ }
132
+ /**
133
+ * Get all model switch history for a session (chronological order)
134
+ */
135
+ getHistory(sessionId) {
136
+ const entryIds = this.index.bySession[sessionId] || [];
137
+ return entryIds
138
+ .map(id => this.index.entries[id])
139
+ .filter(entry => entry !== undefined)
140
+ .sort((a, b) => a.timestamp - b.timestamp);
141
+ }
142
+ /**
143
+ * Clear history for a specific session
144
+ */
145
+ clearHistory(sessionId) {
146
+ const entryIds = this.index.bySession[sessionId] || [];
147
+ // Remove entries
148
+ for (const id of entryIds) {
149
+ delete this.index.entries[id];
150
+ }
151
+ // Remove session list
152
+ delete this.index.bySession[sessionId];
153
+ // Remove from last switch tracker
154
+ this.lastSwitchBySession.delete(sessionId);
155
+ // Schedule write
156
+ this.scheduleWrite();
157
+ }
158
+ /**
159
+ * Get statistics for a session
160
+ */
161
+ getStats(sessionId) {
162
+ const history = this.getHistory(sessionId);
163
+ const switchCount = history.length;
164
+ if (switchCount === 0) {
165
+ return { switchCount: 0, avgDurationMs: null };
166
+ }
167
+ // Calculate average duration (excluding entries without duration)
168
+ const durations = history
169
+ .map(entry => entry.durationMs)
170
+ .filter((d) => d !== undefined);
171
+ const avgDurationMs = durations.length > 0
172
+ ? durations.reduce((sum, d) => sum + d, 0) / durations.length
173
+ : null;
174
+ return { switchCount, avgDurationMs };
175
+ }
176
+ /**
177
+ * Force flush to disk (for graceful shutdown)
178
+ */
179
+ shutdown() {
180
+ if (this.pendingWrite) {
181
+ clearTimeout(this.pendingWrite);
182
+ this.pendingWrite = null;
183
+ }
184
+ this.flushToDisk();
185
+ }
186
+ }
187
+ exports.ModelHistoryManager = ModelHistoryManager;
@@ -12,6 +12,7 @@ class SessionManager {
12
12
  this.activeSessionId = null;
13
13
  this.emitter = null;
14
14
  this.sessionEndCallbacks = [];
15
+ this.modelHistoryManager = null;
15
16
  this.historyManager = historyManager;
16
17
  this.sessionPool = sessionPool;
17
18
  }
@@ -32,6 +33,9 @@ class SessionManager {
32
33
  setMainWindow(window) {
33
34
  this.emitter = new ipc_emitter_1.IPCEmitter(window);
34
35
  }
36
+ setModelHistoryManager(manager) {
37
+ this.modelHistoryManager = manager;
38
+ }
35
39
  initialize() {
36
40
  // Load persisted sessions
37
41
  const state = (0, session_persistence_1.loadSessionState)();
@@ -80,6 +84,7 @@ class SessionManager {
80
84
  if (!(0, session_persistence_1.validateDirectory)(workingDir)) {
81
85
  throw new Error('Invalid working directory');
82
86
  }
87
+ const model = request.model;
83
88
  const id = (0, uuid_1.v4)();
84
89
  const metadata = {
85
90
  id,
@@ -89,6 +94,55 @@ class SessionManager {
89
94
  status: 'starting',
90
95
  createdAt: Date.now(),
91
96
  };
97
+ // Register all callbacks on a CLIManager BEFORE any async operations
98
+ // that produce PTY output (fixes race where Phase 1 fires before callback is set)
99
+ const registerCallbacks = (mgr) => {
100
+ mgr.onModelChange((model) => {
101
+ const session = this.sessions.get(id);
102
+ if (session) {
103
+ const previousModel = session.metadata.currentModel ?? null;
104
+ session.metadata.currentModel = model;
105
+ const event = {
106
+ sessionId: id,
107
+ model,
108
+ previousModel,
109
+ detectedAt: Date.now(),
110
+ };
111
+ // Emit model change event
112
+ this.emitter?.emit('onModelChanged', event);
113
+ // Log to model history
114
+ if (this.modelHistoryManager) {
115
+ this.modelHistoryManager.logSwitch(event);
116
+ }
117
+ // Also emit general session updated
118
+ this.emitter?.emit('onSessionUpdated', session.metadata);
119
+ this.persistState();
120
+ }
121
+ });
122
+ mgr.onOutput((data) => {
123
+ const output = { sessionId: id, data };
124
+ this.emitter?.emit('onSessionOutput', output);
125
+ // Record to history (async, non-blocking)
126
+ this.historyManager.recordOutput(id, data).catch(err => {
127
+ console.error('Failed to record history:', err);
128
+ });
129
+ });
130
+ mgr.onExit((exitCode) => {
131
+ const session = this.sessions.get(id);
132
+ if (session) {
133
+ session.metadata.status = 'exited';
134
+ session.metadata.exitCode = exitCode;
135
+ this.emitter?.emit('onSessionUpdated', session.metadata);
136
+ this.emitter?.emit('onSessionExited', { sessionId: id, exitCode });
137
+ this.persistState();
138
+ this.notifySessionEnd(id);
139
+ // Flush final history buffer
140
+ this.historyManager.onSessionExit(id, exitCode).catch(err => {
141
+ console.error('Failed to finalize session history:', err);
142
+ });
143
+ }
144
+ });
145
+ };
92
146
  // Try to claim from pool first
93
147
  const pooledSession = this.sessionPool.claim();
94
148
  let cliManager;
@@ -96,8 +150,9 @@ class SessionManager {
96
150
  // POOL PATH: Activate pooled session
97
151
  console.log(`[SessionManager] Using pooled session ${pooledSession.id} for ${id}`);
98
152
  cliManager = pooledSession.cliManager;
153
+ registerCallbacks(cliManager);
99
154
  try {
100
- await cliManager.initializeSession(workingDir, request.permissionMode);
155
+ await cliManager.initializeSession(workingDir, request.permissionMode, model);
101
156
  }
102
157
  catch (err) {
103
158
  // Activation failed, fall back to direct creation
@@ -106,7 +161,9 @@ class SessionManager {
106
161
  cliManager = new cli_manager_1.CLIManager({
107
162
  workingDirectory: workingDir,
108
163
  permissionMode: request.permissionMode,
164
+ model,
109
165
  });
166
+ registerCallbacks(cliManager);
110
167
  await cliManager.spawn();
111
168
  }
112
169
  }
@@ -116,34 +173,11 @@ class SessionManager {
116
173
  cliManager = new cli_manager_1.CLIManager({
117
174
  workingDirectory: workingDir,
118
175
  permissionMode: request.permissionMode,
176
+ model,
119
177
  });
178
+ registerCallbacks(cliManager);
120
179
  cliManager.spawn();
121
180
  }
122
- // Set up output handler
123
- cliManager.onOutput((data) => {
124
- const output = { sessionId: id, data };
125
- this.emitter?.emit('onSessionOutput', output);
126
- // Record to history (async, non-blocking)
127
- this.historyManager.recordOutput(id, data).catch(err => {
128
- console.error('Failed to record history:', err);
129
- });
130
- });
131
- // Set up exit handler
132
- cliManager.onExit((exitCode) => {
133
- const session = this.sessions.get(id);
134
- if (session) {
135
- session.metadata.status = 'exited';
136
- session.metadata.exitCode = exitCode;
137
- this.emitter?.emit('onSessionUpdated', session.metadata);
138
- this.emitter?.emit('onSessionExited', { sessionId: id, exitCode });
139
- this.persistState();
140
- this.notifySessionEnd(id);
141
- // Flush final history buffer
142
- this.historyManager.onSessionExit(id, exitCode).catch(err => {
143
- console.error('Failed to finalize session history:', err);
144
- });
145
- }
146
- });
147
181
  // Update history metadata with session details
148
182
  this.historyManager.updateSessionMetadata(id, metadata.name, workingDir);
149
183
  // Store session
@@ -231,6 +265,28 @@ class SessionManager {
231
265
  permissionMode: session.metadata.permissionMode,
232
266
  });
233
267
  // Set up handlers
268
+ cliManager.onModelChange((model) => {
269
+ const session = this.sessions.get(sessionId);
270
+ if (session) {
271
+ const previousModel = session.metadata.currentModel ?? null;
272
+ session.metadata.currentModel = model;
273
+ const event = {
274
+ sessionId,
275
+ model,
276
+ previousModel,
277
+ detectedAt: Date.now(),
278
+ };
279
+ // Emit model change event
280
+ this.emitter?.emit('onModelChanged', event);
281
+ // Log to model history
282
+ if (this.modelHistoryManager) {
283
+ this.modelHistoryManager.logSwitch(event);
284
+ }
285
+ // Also emit general session updated
286
+ this.emitter?.emit('onSessionUpdated', session.metadata);
287
+ this.persistState();
288
+ }
289
+ });
234
290
  cliManager.onOutput((data) => {
235
291
  const output = { sessionId, data };
236
292
  this.emitter?.emit('onSessionOutput', output);
@@ -256,6 +312,7 @@ class SessionManager {
256
312
  session.cliManager = cliManager;
257
313
  session.metadata.status = 'starting';
258
314
  session.metadata.exitCode = undefined;
315
+ session.metadata.currentModel = undefined; // Clear stale model — Phase 1 will re-detect
259
316
  try {
260
317
  cliManager.spawn();
261
318
  session.metadata.status = 'running';
@@ -82,6 +82,7 @@ function saveSessionState(sessions, activeSessionId) {
82
82
  status: session.status,
83
83
  createdAt: session.createdAt,
84
84
  exitCode: session.exitCode,
85
+ currentModel: session.currentModel,
85
86
  })),
86
87
  activeSessionId,
87
88
  lastModified: Date.now(),
@@ -19,7 +19,10 @@ class SessionPool {
19
19
  if (this.isInitialized || !this.config.enabled || this.config.size === 0) {
20
20
  return;
21
21
  }
22
- console.log(`[SessionPool] Initializing pool with size ${this.config.size}`);
22
+ try {
23
+ console.log(`[SessionPool] Initializing pool with size ${this.config.size}`);
24
+ }
25
+ catch (err) { /* Ignore EPIPE */ }
23
26
  // Pre-spawn configured number of idle sessions
24
27
  const spawnPromises = [];
25
28
  for (let i = 0; i < this.config.size; i++) {
@@ -29,7 +32,12 @@ class SessionPool {
29
32
  this.isInitialized = true;
30
33
  // Start periodic cleanup of stale sessions
31
34
  this.startCleanupInterval();
32
- console.log(`[SessionPool] Initialized with ${this.idleQueue.length} idle sessions`);
35
+ try {
36
+ console.log(`[SessionPool] Initialized with ${this.idleQueue.length} idle sessions`);
37
+ }
38
+ catch (err) {
39
+ // Ignore EPIPE errors from console.log
40
+ }
33
41
  }
34
42
  /**
35
43
  * Claim an idle session from the pool.
@@ -43,7 +51,12 @@ class SessionPool {
43
51
  if (!pooled) {
44
52
  return null;
45
53
  }
46
- console.log(`[SessionPool] Claimed session ${pooled.id}, ${this.idleQueue.length} remaining`);
54
+ try {
55
+ console.log(`[SessionPool] Claimed session ${pooled.id}, ${this.idleQueue.length} remaining`);
56
+ }
57
+ catch (err) {
58
+ // Ignore EPIPE errors from console.log
59
+ }
47
60
  // Trigger async replenishment (non-blocking)
48
61
  this.replenishPool().catch(err => {
49
62
  console.error('[SessionPool] Failed to replenish pool:', err);
@@ -61,16 +74,25 @@ class SessionPool {
61
74
  console.log(`[SessionPool] Config updated:`, this.config);
62
75
  // If disabled, destroy all idle sessions
63
76
  if (!this.config.enabled && oldEnabled) {
64
- console.log('[SessionPool] Disabled - destroying all idle sessions');
77
+ try {
78
+ console.log('[SessionPool] Disabled - destroying all idle sessions');
79
+ }
80
+ catch (err) { /* Ignore EPIPE */ }
65
81
  this.destroyAllIdle();
66
82
  this.stopCleanupInterval();
67
83
  return;
68
84
  }
69
85
  // If enabled but was disabled, initialize
70
86
  if (this.config.enabled && !oldEnabled) {
71
- console.log('[SessionPool] Enabled - initializing pool');
87
+ try {
88
+ console.log('[SessionPool] Enabled - initializing pool');
89
+ }
90
+ catch (err) { /* Ignore EPIPE */ }
72
91
  this.initialize().catch(err => {
73
- console.error('[SessionPool] Failed to initialize after enable:', err);
92
+ try {
93
+ console.error('[SessionPool] Failed to initialize after enable:', err);
94
+ }
95
+ catch (e) { /* Ignore EPIPE */ }
74
96
  });
75
97
  return;
76
98
  }
@@ -78,10 +100,16 @@ class SessionPool {
78
100
  if (this.config.enabled && this.config.size > oldSize) {
79
101
  const diff = this.config.size - this.idleQueue.length;
80
102
  if (diff > 0) {
81
- console.log(`[SessionPool] Size increased - spawning ${diff} idle sessions`);
103
+ try {
104
+ console.log(`[SessionPool] Size increased - spawning ${diff} idle sessions`);
105
+ }
106
+ catch (err) { /* Ignore EPIPE */ }
82
107
  for (let i = 0; i < diff; i++) {
83
108
  this.createIdleSession().catch(err => {
84
- console.error('[SessionPool] Failed to spawn idle session:', err);
109
+ try {
110
+ console.error('[SessionPool] Failed to spawn idle session:', err);
111
+ }
112
+ catch (e) { /* Ignore EPIPE */ }
85
113
  });
86
114
  }
87
115
  }
@@ -90,7 +118,10 @@ class SessionPool {
90
118
  if (this.config.enabled && this.config.size < oldSize) {
91
119
  const excess = this.idleQueue.length - this.config.size;
92
120
  if (excess > 0) {
93
- console.log(`[SessionPool] Size decreased - destroying ${excess} idle sessions`);
121
+ try {
122
+ console.log(`[SessionPool] Size decreased - destroying ${excess} idle sessions`);
123
+ }
124
+ catch (err) { /* Ignore EPIPE */ }
94
125
  for (let i = 0; i < excess; i++) {
95
126
  const pooled = this.idleQueue.pop();
96
127
  if (pooled) {
@@ -78,6 +78,8 @@ function getDefaultSettings() {
78
78
  excludePatterns: [],
79
79
  scanTimeoutMs: 30000,
80
80
  },
81
+ defaultModel: 'sonnet',
82
+ modelPreset: 'balanced',
81
83
  };
82
84
  }
83
85
  // Validate split view layout by removing invalid session references
@@ -89,24 +91,40 @@ function validateSplitViewLayout(layout, validSessionIds) {
89
91
  : null;
90
92
  return { ...layout, sessionId };
91
93
  }
92
- // Branch: recursively validate children
93
- return {
94
- ...layout,
95
- children: [
96
- validateSplitViewLayout(layout.children[0], validSessionIds),
97
- validateSplitViewLayout(layout.children[1], validSessionIds)
98
- ]
99
- };
94
+ if (layout.type === 'branch') {
95
+ // Branch: recursively validate children
96
+ return {
97
+ ...layout,
98
+ children: [
99
+ validateSplitViewLayout(layout.children[0], validSessionIds),
100
+ validateSplitViewLayout(layout.children[1], validSessionIds)
101
+ ]
102
+ };
103
+ }
104
+ if (layout.type === 'grid') {
105
+ // Grid: recursively validate all children
106
+ return {
107
+ ...layout,
108
+ children: layout.children.map(child => validateSplitViewLayout(child, validSessionIds))
109
+ };
110
+ }
111
+ return layout;
100
112
  }
101
113
  // Get all pane IDs from layout tree
102
114
  function getAllPaneIdsFromLayout(layout) {
103
115
  if (layout.type === 'leaf') {
104
116
  return [layout.paneId];
105
117
  }
106
- return [
107
- ...getAllPaneIdsFromLayout(layout.children[0]),
108
- ...getAllPaneIdsFromLayout(layout.children[1])
109
- ];
118
+ if (layout.type === 'branch') {
119
+ return [
120
+ ...getAllPaneIdsFromLayout(layout.children[0]),
121
+ ...getAllPaneIdsFromLayout(layout.children[1])
122
+ ];
123
+ }
124
+ if (layout.type === 'grid') {
125
+ return layout.children.flatMap(child => getAllPaneIdsFromLayout(child));
126
+ }
127
+ return [];
110
128
  }
111
129
  // Validate layout structure (ensure it's well-formed)
112
130
  function isValidLayoutStructure(layout) {
@@ -126,6 +144,27 @@ function isValidLayoutStructure(layout) {
126
144
  }
127
145
  return isValidLayoutStructure(layout.children[0]) && isValidLayoutStructure(layout.children[1]);
128
146
  }
147
+ if (layout.type === 'grid') {
148
+ if (typeof layout.id !== 'string' || layout.id.length === 0) {
149
+ return false;
150
+ }
151
+ if (layout.direction !== 'horizontal' && layout.direction !== 'vertical') {
152
+ return false;
153
+ }
154
+ if (!Array.isArray(layout.children) || layout.children.length === 0) {
155
+ return false;
156
+ }
157
+ if (!Array.isArray(layout.sizes) || layout.sizes.length !== layout.children.length) {
158
+ return false;
159
+ }
160
+ // Validate sizes sum to approximately 100
161
+ const sum = layout.sizes.reduce((a, b) => a + b, 0);
162
+ if (Math.abs(sum - 100) > 0.5) {
163
+ return false;
164
+ }
165
+ // Validate all children
166
+ return layout.children.every(child => isValidLayoutStructure(child));
167
+ }
129
168
  return false;
130
169
  }
131
170
  catch {
@@ -339,6 +378,22 @@ class SettingsManager {
339
378
  this.settings.autoLayoutTeams = enabled;
340
379
  saveSettings(this.settings);
341
380
  }
381
+ updateUIMode(mode) {
382
+ this.settings.uiMode = mode;
383
+ saveSettings(this.settings);
384
+ }
385
+ updateDefaultModel(model) {
386
+ this.settings.defaultModel = model;
387
+ saveSettings(this.settings);
388
+ }
389
+ updateLastUsedLayoutPreset(presetId) {
390
+ this.settings.lastUsedLayoutPresetId = presetId;
391
+ saveSettings(this.settings);
392
+ }
393
+ markAsLaunched() {
394
+ this.settings.hasLaunchedBefore = true;
395
+ saveSettings(this.settings);
396
+ }
342
397
  validateSplitViewState(validSessionIds) {
343
398
  if (!this.settings.splitViewState) {
344
399
  return;