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
|
@@ -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';
|
|
@@ -19,7 +19,10 @@ class SessionPool {
|
|
|
19
19
|
if (this.isInitialized || !this.config.enabled || this.config.size === 0) {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
try {
|
|
88
|
+
console.log('[SessionPool] Enabled - initializing pool');
|
|
89
|
+
}
|
|
90
|
+
catch (err) { /* Ignore EPIPE */ }
|
|
72
91
|
this.initialize().catch(err => {
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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;
|