@sumicom/quicksave 0.8.1 → 0.8.2
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/README.md +4 -0
- package/dist/ai/cardBuilder.d.ts +2 -0
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +62 -0
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +25 -1
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +77 -53
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/codexLogin.d.ts +57 -0
- package/dist/ai/codexLogin.d.ts.map +1 -0
- package/dist/ai/codexLogin.js +240 -0
- package/dist/ai/codexLogin.js.map +1 -0
- package/dist/ai/codexSdkProvider.d.ts.map +1 -1
- package/dist/ai/codexSdkProvider.js +66 -5
- package/dist/ai/codexSdkProvider.js.map +1 -1
- package/dist/ai/provider.d.ts +5 -0
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sessionManager.d.ts +6 -0
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +90 -4
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/files/fileBrowser.d.ts +25 -0
- package/dist/files/fileBrowser.d.ts.map +1 -0
- package/dist/files/fileBrowser.js +186 -0
- package/dist/files/fileBrowser.js.map +1 -0
- package/dist/handlers/messageHandler.d.ts +77 -5
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +399 -53
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +57 -4
- package/dist/service/run.js.map +1 -1
- package/dist/service/types.js +1 -1
- package/dist/terminal/terminalManager.d.ts +48 -0
- package/dist/terminal/terminalManager.d.ts.map +1 -0
- package/dist/terminal/terminalManager.js +244 -0
- package/dist/terminal/terminalManager.js.map +1 -0
- package/package.json +4 -3
|
@@ -7,18 +7,53 @@ import { CommitSummaryStateStore } from '../ai/commitSummaryStore.js';
|
|
|
7
7
|
import { SessionManager } from '../ai/sessionManager.js';
|
|
8
8
|
import { ClaudeCodeProvider } from '../ai/claudeCodeProvider.js';
|
|
9
9
|
import { CodexSdkProvider } from '../ai/codexSdkProvider.js';
|
|
10
|
+
import { CodexLoginManager } from '../ai/codexLogin.js';
|
|
11
|
+
import { getTerminalManager } from '../terminal/terminalManager.js';
|
|
12
|
+
import { getFileBrowser } from '../files/fileBrowser.js';
|
|
10
13
|
import { getSessionRegistry } from '../ai/sessionRegistry.js';
|
|
11
14
|
import { enrichEntry } from '../ai/enrichEntry.js';
|
|
12
15
|
import { getEventStore } from '../storage/eventStore.js';
|
|
13
16
|
import { readdir, stat, readFile } from 'fs/promises';
|
|
14
|
-
import { existsSync } from 'fs';
|
|
17
|
+
import { existsSync, watch as fsWatch } from 'fs';
|
|
15
18
|
import { join, dirname, basename } from 'path';
|
|
16
19
|
import { homedir } from 'os';
|
|
17
20
|
const VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
18
|
-
|
|
21
|
+
// Local file read is essentially free + fs.watch invalidates on cache writes.
|
|
22
|
+
// 5 min is just a safety net for missed-event cases (NFS, container fs, etc.).
|
|
23
|
+
const CODEX_MODELS_FILE_TTL_MS = 5 * 60 * 1000;
|
|
24
|
+
// API path hits OpenAI; keep its own longer TTL to avoid hammering the
|
|
25
|
+
// endpoint when the user has no local Codex CLI cache file at all.
|
|
26
|
+
const CODEX_MODELS_API_TTL_MS = 60 * 60 * 1000;
|
|
27
|
+
// fs.watch on ~/.codex fires multiple events per write (rename + change on
|
|
28
|
+
// many filesystems). Coalesce into one refresh per burst.
|
|
29
|
+
const CODEX_MODELS_WATCH_DEBOUNCE_MS = 500;
|
|
30
|
+
function codexModelListsEqual(a, b) {
|
|
31
|
+
if (a === b)
|
|
32
|
+
return true;
|
|
33
|
+
if (!a || !b)
|
|
34
|
+
return false;
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
for (let i = 0; i < a.length; i++) {
|
|
38
|
+
if (a[i].id !== b[i].id || a[i].name !== b[i].name)
|
|
39
|
+
return false;
|
|
40
|
+
const aE = a[i].reasoningEfforts;
|
|
41
|
+
const bE = b[i].reasoningEfforts;
|
|
42
|
+
if (aE === bE)
|
|
43
|
+
continue;
|
|
44
|
+
if (!aE || !bE)
|
|
45
|
+
return false;
|
|
46
|
+
if (aE.length !== bE.length)
|
|
47
|
+
return false;
|
|
48
|
+
for (let j = 0; j < aE.length; j++)
|
|
49
|
+
if (aE[j] !== bE[j])
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
19
54
|
export class MessageHandler {
|
|
20
55
|
repos;
|
|
21
|
-
agentVersion = '0.8.
|
|
56
|
+
agentVersion = '0.8.2';
|
|
22
57
|
defaultRepoPath;
|
|
23
58
|
clientRepos = new Map(); // peerAddress -> repoPath
|
|
24
59
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
@@ -38,9 +73,15 @@ export class MessageHandler {
|
|
|
38
73
|
versionCheckInFlight = null;
|
|
39
74
|
codexModelsCache = null;
|
|
40
75
|
codexModelsCheckInFlight = null;
|
|
76
|
+
codexModelsUpdateHandler = null;
|
|
77
|
+
codexModelsWatcher = null;
|
|
78
|
+
codexModelsWatchDebounce = null;
|
|
79
|
+
/** Override for tests; defaults to `~/.codex`. */
|
|
80
|
+
codexCacheDir;
|
|
81
|
+
codexLoginManager = new CodexLoginManager();
|
|
41
82
|
onHistoryUpdated;
|
|
42
83
|
productionBuild;
|
|
43
|
-
constructor(repos, _license, codingPaths, productionBuild = false) {
|
|
84
|
+
constructor(repos, _license, codingPaths, productionBuild = false, options) {
|
|
44
85
|
this.repos = new Map();
|
|
45
86
|
for (const repo of repos) {
|
|
46
87
|
this.repos.set(repo.path, new GitOperations(repo.path));
|
|
@@ -48,6 +89,7 @@ export class MessageHandler {
|
|
|
48
89
|
this.availableRepos = repos;
|
|
49
90
|
this.defaultRepoPath = repos.length > 0 ? repos[0].path : '';
|
|
50
91
|
this.productionBuild = productionBuild;
|
|
92
|
+
this.codexCacheDir = options?.codexCacheDir ?? join(homedir(), '.codex');
|
|
51
93
|
// Load explicit coding paths only (repos and coding paths are independent)
|
|
52
94
|
if (codingPaths) {
|
|
53
95
|
for (const p of codingPaths) {
|
|
@@ -92,48 +134,46 @@ export class MessageHandler {
|
|
|
92
134
|
return this.versionCheckInFlight;
|
|
93
135
|
}
|
|
94
136
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
137
|
+
* Resolve the Codex model list for the current user.
|
|
138
|
+
*
|
|
139
|
+
* Priority:
|
|
140
|
+
* 1. `~/.codex/models_cache.json` — the Codex CLI's own cache of
|
|
141
|
+
* models the signed-in account can actually use. This is the
|
|
142
|
+
* authoritative list for ChatGPT-OAuth logins (the common case)
|
|
143
|
+
* and is kept fresh by the CLI itself. Reads are essentially free,
|
|
144
|
+
* so we use a short 5min in-memory TTL — and `startCodexModelsWatcher`
|
|
145
|
+
* invalidates the cache the moment the file is rewritten.
|
|
146
|
+
* 2. `OPENAI_API_KEY` env var + OpenAI `/v1/models` — fallback for
|
|
147
|
+
* API-key users, who are rare but still supported. Held to a 1h
|
|
148
|
+
* sub-TTL so we don't hammer OpenAI when the local cache is absent.
|
|
149
|
+
*
|
|
150
|
+
* Returns `null` when neither source is available (not logged in and no
|
|
151
|
+
* API key set). Concurrent calls are deduped.
|
|
98
152
|
*/
|
|
99
153
|
async fetchCodexModels(force = false) {
|
|
100
|
-
if (!force && this.codexModelsCache
|
|
101
|
-
|
|
102
|
-
|
|
154
|
+
if (!force && this.codexModelsCache) {
|
|
155
|
+
const ttl = this.codexModelsCache.source === 'file'
|
|
156
|
+
? CODEX_MODELS_FILE_TTL_MS
|
|
157
|
+
: CODEX_MODELS_API_TTL_MS;
|
|
158
|
+
if (Date.now() - this.codexModelsCache.checkedAt < ttl) {
|
|
159
|
+
return this.codexModelsCache.models;
|
|
160
|
+
}
|
|
103
161
|
}
|
|
104
162
|
if (this.codexModelsCheckInFlight)
|
|
105
163
|
return this.codexModelsCheckInFlight;
|
|
106
164
|
this.codexModelsCheckInFlight = (async () => {
|
|
107
165
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const authPath = join(homedir(), '.codex', 'auth.json');
|
|
113
|
-
const raw = await readFile(authPath, 'utf-8');
|
|
114
|
-
const auth = JSON.parse(raw);
|
|
115
|
-
apiKey = auth.OPENAI_API_KEY || auth.tokens?.access_token;
|
|
116
|
-
}
|
|
117
|
-
catch { /* no auth file */ }
|
|
166
|
+
const fromCache = await this.readCodexModelsCacheFile();
|
|
167
|
+
if (fromCache && fromCache.length > 0) {
|
|
168
|
+
this.codexModelsCache = { models: fromCache, checkedAt: Date.now(), source: 'file' };
|
|
169
|
+
return fromCache;
|
|
118
170
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!res.ok)
|
|
126
|
-
return null;
|
|
127
|
-
const data = await res.json();
|
|
128
|
-
if (!Array.isArray(data.data))
|
|
129
|
-
return null;
|
|
130
|
-
// Filter to chat-capable models (gpt-*, o*) and sort descending
|
|
131
|
-
const models = data.data
|
|
132
|
-
.filter((m) => /^(gpt-|o\d)/.test(m.id))
|
|
133
|
-
.map((m) => ({ id: m.id, name: m.id }))
|
|
134
|
-
.sort((a, b) => b.id.localeCompare(a.id));
|
|
135
|
-
this.codexModelsCache = { models, checkedAt: Date.now() };
|
|
136
|
-
return models;
|
|
171
|
+
const fromApi = await this.fetchCodexModelsFromOpenAI();
|
|
172
|
+
if (fromApi && fromApi.length > 0) {
|
|
173
|
+
this.codexModelsCache = { models: fromApi, checkedAt: Date.now(), source: 'api' };
|
|
174
|
+
return fromApi;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
137
177
|
}
|
|
138
178
|
catch {
|
|
139
179
|
return null;
|
|
@@ -144,6 +184,141 @@ export class MessageHandler {
|
|
|
144
184
|
})();
|
|
145
185
|
return this.codexModelsCheckInFlight;
|
|
146
186
|
}
|
|
187
|
+
/** Snapshot accessor for the bus `/codex/models` subscription. */
|
|
188
|
+
getCachedCodexModels() {
|
|
189
|
+
return this.codexModelsCache?.models ?? [];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Wire the daemon's bus broadcast for unsolicited model-list pushes.
|
|
193
|
+
* Called once during daemon boot; the watcher invokes this on cache change.
|
|
194
|
+
*/
|
|
195
|
+
setCodexModelsUpdateHandler(handler) {
|
|
196
|
+
this.codexModelsUpdateHandler = handler;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Watch the Codex CLI cache directory and refresh the in-memory model list
|
|
200
|
+
* the moment the CLI rewrites it (typically after a `codex` startup or
|
|
201
|
+
* upgrade). Tolerant of a missing `~/.codex` directory — falls back to TTL
|
|
202
|
+
* alone if the watch can't be set up. Idempotent.
|
|
203
|
+
*/
|
|
204
|
+
startCodexModelsWatcher() {
|
|
205
|
+
if (this.codexModelsWatcher)
|
|
206
|
+
return;
|
|
207
|
+
// Prime the in-memory cache so the first /codex/models subscriber gets a
|
|
208
|
+
// populated snapshot instead of an empty one (the handshake also fires
|
|
209
|
+
// this; dedup via codexModelsCheckInFlight makes it cheap).
|
|
210
|
+
void this.fetchCodexModels().catch(() => { });
|
|
211
|
+
try {
|
|
212
|
+
// Watch the directory rather than the file directly so we react to
|
|
213
|
+
// first-time creation too. Many editors / cli tools rewrite atomically
|
|
214
|
+
// (write tmp + rename), which fires events for the directory, not the
|
|
215
|
+
// old file inode.
|
|
216
|
+
this.codexModelsWatcher = fsWatch(this.codexCacheDir, (_event, filename) => {
|
|
217
|
+
if (filename && filename !== 'models_cache.json')
|
|
218
|
+
return;
|
|
219
|
+
this.scheduleCodexModelsRefresh();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Directory missing or unwatchable — quietly skip; the 5min TTL still
|
|
224
|
+
// catches the next refresh once a request comes in.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Tear down the fs.watch handle (test cleanup). */
|
|
228
|
+
stopCodexModelsWatcher() {
|
|
229
|
+
if (this.codexModelsWatchDebounce) {
|
|
230
|
+
clearTimeout(this.codexModelsWatchDebounce);
|
|
231
|
+
this.codexModelsWatchDebounce = null;
|
|
232
|
+
}
|
|
233
|
+
if (this.codexModelsWatcher) {
|
|
234
|
+
this.codexModelsWatcher.close();
|
|
235
|
+
this.codexModelsWatcher = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
scheduleCodexModelsRefresh() {
|
|
239
|
+
if (this.codexModelsWatchDebounce)
|
|
240
|
+
clearTimeout(this.codexModelsWatchDebounce);
|
|
241
|
+
this.codexModelsWatchDebounce = setTimeout(() => {
|
|
242
|
+
this.codexModelsWatchDebounce = null;
|
|
243
|
+
void this.refreshCodexModelsAndPublish();
|
|
244
|
+
}, CODEX_MODELS_WATCH_DEBOUNCE_MS);
|
|
245
|
+
}
|
|
246
|
+
async refreshCodexModelsAndPublish() {
|
|
247
|
+
const before = this.codexModelsCache?.models;
|
|
248
|
+
const after = await this.fetchCodexModels(/* force */ true);
|
|
249
|
+
if (!after)
|
|
250
|
+
return;
|
|
251
|
+
if (codexModelListsEqual(before, after))
|
|
252
|
+
return;
|
|
253
|
+
this.codexModelsUpdateHandler?.(after);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Read models from the Codex CLI's local cache. File shape (as of codex
|
|
257
|
+
* CLI v0.125): `{ models: [{ slug, display_name, visibility, supported_in_api, ... }] }`.
|
|
258
|
+
* We surface only entries that are listable and API-supported, so hidden
|
|
259
|
+
* internal models (e.g. `codex-auto-review`) don't leak into the picker.
|
|
260
|
+
*/
|
|
261
|
+
async readCodexModelsCacheFile() {
|
|
262
|
+
try {
|
|
263
|
+
const cachePath = join(this.codexCacheDir, 'models_cache.json');
|
|
264
|
+
const raw = await readFile(cachePath, 'utf-8');
|
|
265
|
+
const parsed = JSON.parse(raw);
|
|
266
|
+
if (!Array.isArray(parsed.models))
|
|
267
|
+
return null;
|
|
268
|
+
const models = parsed.models
|
|
269
|
+
.filter((m) => m.slug && m.visibility === 'list' && m.supported_in_api !== false)
|
|
270
|
+
.map((m) => ({ id: m.slug, name: m.display_name || m.slug }));
|
|
271
|
+
return models;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Fallback for API-key users (no ChatGPT OAuth). */
|
|
278
|
+
async fetchCodexModelsFromOpenAI() {
|
|
279
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
280
|
+
if (!apiKey)
|
|
281
|
+
return null;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch('https://api.openai.com/v1/models', {
|
|
284
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
285
|
+
signal: AbortSignal.timeout(10_000),
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
return null;
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
if (!Array.isArray(data.data))
|
|
291
|
+
return null;
|
|
292
|
+
return data.data
|
|
293
|
+
.filter((m) => /^(gpt-|o\d)/.test(m.id))
|
|
294
|
+
.map((m) => ({ id: m.id, name: m.id }))
|
|
295
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Best-effort check for whether the local Codex CLI has valid auth.
|
|
303
|
+
* Matches how `fetchCodexModels` resolves credentials, so the PWA can
|
|
304
|
+
* prompt for login when this returns false. Does not validate the
|
|
305
|
+
* token — just checks that a credential source exists.
|
|
306
|
+
*/
|
|
307
|
+
async getCodexLoginStatus() {
|
|
308
|
+
if (process.env.OPENAI_API_KEY)
|
|
309
|
+
return { loggedIn: true, method: 'api-key' };
|
|
310
|
+
try {
|
|
311
|
+
const authPath = join(this.codexCacheDir, 'auth.json');
|
|
312
|
+
const raw = await readFile(authPath, 'utf-8');
|
|
313
|
+
const auth = JSON.parse(raw);
|
|
314
|
+
if (auth.OPENAI_API_KEY)
|
|
315
|
+
return { loggedIn: true, method: 'api-key' };
|
|
316
|
+
if (auth.tokens?.access_token)
|
|
317
|
+
return { loggedIn: true, method: 'chatgpt' };
|
|
318
|
+
}
|
|
319
|
+
catch { /* no auth file */ }
|
|
320
|
+
return { loggedIn: false };
|
|
321
|
+
}
|
|
147
322
|
addRepo(repo) {
|
|
148
323
|
if (this.repos.has(repo.path))
|
|
149
324
|
return;
|
|
@@ -159,6 +334,12 @@ export class MessageHandler {
|
|
|
159
334
|
if (this.defaultRepoPath === path) {
|
|
160
335
|
this.defaultRepoPath = this.availableRepos.length > 0 ? this.availableRepos[0].path : '';
|
|
161
336
|
}
|
|
337
|
+
// Drop stale per-peer pins to this path so the next handshake-ack
|
|
338
|
+
// doesn't return a deleted repo.
|
|
339
|
+
for (const [peer, repo] of this.clientRepos) {
|
|
340
|
+
if (repo === path)
|
|
341
|
+
this.clientRepos.delete(peer);
|
|
342
|
+
}
|
|
162
343
|
}
|
|
163
344
|
getClientRepoPath(peerAddress) {
|
|
164
345
|
return this.clientRepos.get(peerAddress) || this.defaultRepoPath;
|
|
@@ -185,7 +366,14 @@ export class MessageHandler {
|
|
|
185
366
|
}
|
|
186
367
|
}
|
|
187
368
|
removeClient(peerAddress) {
|
|
188
|
-
|
|
369
|
+
// Intentionally keep `clientRepos[peer]`: peer identity is a stable
|
|
370
|
+
// public key, and the PWA keeps its selected repo across reconnects
|
|
371
|
+
// (e.g., background → resume). Clearing would force the next
|
|
372
|
+
// handshake-ack to return `defaultRepoPath`, which the PWA would then
|
|
373
|
+
// hydrate into gitStore before its post-reconnect `agent:switch-repo`
|
|
374
|
+
// can land — racing `git:status` requests stamped with the default
|
|
375
|
+
// path would pass the REPO_MISMATCH guard and paint the default
|
|
376
|
+
// repo's (typically clean) status over the user's real repo view.
|
|
189
377
|
for (const [repoPath, holder] of this.repoLocks) {
|
|
190
378
|
if (holder === peerAddress) {
|
|
191
379
|
this.repoLocks.delete(repoPath);
|
|
@@ -195,6 +383,7 @@ export class MessageHandler {
|
|
|
195
383
|
// They persist until the user reconnects and explicitly responds.
|
|
196
384
|
}
|
|
197
385
|
cleanup() {
|
|
386
|
+
this.stopCodexModelsWatcher();
|
|
198
387
|
this.claudeService.cleanup();
|
|
199
388
|
}
|
|
200
389
|
getActiveSessionCount() {
|
|
@@ -227,7 +416,9 @@ export class MessageHandler {
|
|
|
227
416
|
return this.aiService;
|
|
228
417
|
}
|
|
229
418
|
async handleMessage(message, peerAddress = 'default') {
|
|
230
|
-
const isVerbose = message.type.startsWith('claude:') ||
|
|
419
|
+
const isVerbose = message.type.startsWith('claude:') ||
|
|
420
|
+
message.type.startsWith('agent:add') ||
|
|
421
|
+
message.type.startsWith('project:');
|
|
231
422
|
if (isVerbose) {
|
|
232
423
|
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
233
424
|
}
|
|
@@ -330,6 +521,12 @@ export class MessageHandler {
|
|
|
330
521
|
return this.handleAgentRestart(message);
|
|
331
522
|
case 'codex:list-models':
|
|
332
523
|
return this.handleCodexListModels(message);
|
|
524
|
+
case 'codex:login-start':
|
|
525
|
+
return this.handleCodexLoginStart(message);
|
|
526
|
+
case 'codex:login-status':
|
|
527
|
+
return this.handleCodexLoginStatus(message);
|
|
528
|
+
case 'codex:login-cancel':
|
|
529
|
+
return this.handleCodexLoginCancel(message);
|
|
333
530
|
// Claude Code SDK
|
|
334
531
|
case 'claude:start':
|
|
335
532
|
return this.handleClaudeStart(message, peerAddress);
|
|
@@ -365,6 +562,20 @@ export class MessageHandler {
|
|
|
365
562
|
return this.handleDeleteProject(message);
|
|
366
563
|
case 'push:subscription-offer':
|
|
367
564
|
return this.handlePushSubscriptionOffer(message);
|
|
565
|
+
case 'terminal:create':
|
|
566
|
+
return this.handleTerminalCreate(message);
|
|
567
|
+
case 'terminal:input':
|
|
568
|
+
return this.handleTerminalInput(message);
|
|
569
|
+
case 'terminal:resize':
|
|
570
|
+
return this.handleTerminalResize(message);
|
|
571
|
+
case 'terminal:close':
|
|
572
|
+
return this.handleTerminalClose(message);
|
|
573
|
+
case 'terminal:rename':
|
|
574
|
+
return this.handleTerminalRename(message);
|
|
575
|
+
case 'files:list':
|
|
576
|
+
return this.handleFilesList(message);
|
|
577
|
+
case 'files:read':
|
|
578
|
+
return this.handleFilesRead(message);
|
|
368
579
|
default:
|
|
369
580
|
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
370
581
|
}
|
|
@@ -1316,17 +1527,52 @@ export class MessageHandler {
|
|
|
1316
1527
|
}
|
|
1317
1528
|
async handleCodexListModels(message) {
|
|
1318
1529
|
try {
|
|
1319
|
-
const models = await
|
|
1320
|
-
|
|
1530
|
+
const [models, auth] = await Promise.all([
|
|
1531
|
+
this.fetchCodexModels(/* force */ true),
|
|
1532
|
+
this.getCodexLoginStatus(),
|
|
1533
|
+
]);
|
|
1534
|
+
const response = createMessage('codex:list-models:response', { models: models ?? [], loggedIn: auth.loggedIn });
|
|
1321
1535
|
response.id = message.id;
|
|
1322
1536
|
return response;
|
|
1323
1537
|
}
|
|
1324
1538
|
catch (error) {
|
|
1325
|
-
const response = createMessage('codex:list-models:response', {
|
|
1539
|
+
const response = createMessage('codex:list-models:response', {
|
|
1540
|
+
models: [],
|
|
1541
|
+
error: error instanceof Error ? error.message : 'Failed to fetch models',
|
|
1542
|
+
loggedIn: false,
|
|
1543
|
+
});
|
|
1326
1544
|
response.id = message.id;
|
|
1327
1545
|
return response;
|
|
1328
1546
|
}
|
|
1329
1547
|
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Kick off the Codex device-auth OAuth flow on the daemon. Returns the
|
|
1550
|
+
* one-time verification URL + user code so the PWA can display them — the
|
|
1551
|
+
* user typically completes this on a different device (e.g. phone).
|
|
1552
|
+
*/
|
|
1553
|
+
async handleCodexLoginStart(message) {
|
|
1554
|
+
const state = await this.codexLoginManager.start();
|
|
1555
|
+
const ok = state.loggedIn || Boolean(state.userCode && state.verificationUrl);
|
|
1556
|
+
const response = createMessage('codex:login-start:response', { ok, ...state });
|
|
1557
|
+
response.id = message.id;
|
|
1558
|
+
return response;
|
|
1559
|
+
}
|
|
1560
|
+
async handleCodexLoginStatus(message) {
|
|
1561
|
+
const state = await this.codexLoginManager.getStatus();
|
|
1562
|
+
const response = createMessage('codex:login-status:response', state);
|
|
1563
|
+
response.id = message.id;
|
|
1564
|
+
return response;
|
|
1565
|
+
}
|
|
1566
|
+
async handleCodexLoginCancel(message) {
|
|
1567
|
+
this.codexLoginManager.cancel();
|
|
1568
|
+
const response = createMessage('codex:login-cancel:response', { ok: true });
|
|
1569
|
+
response.id = message.id;
|
|
1570
|
+
return response;
|
|
1571
|
+
}
|
|
1572
|
+
/** Expose for the daemon to wire login-state updates into bus broadcasts. */
|
|
1573
|
+
getCodexLoginManager() {
|
|
1574
|
+
return this.codexLoginManager;
|
|
1575
|
+
}
|
|
1330
1576
|
// ============================================================================
|
|
1331
1577
|
// Claude Code SDK Handlers
|
|
1332
1578
|
// ============================================================================
|
|
@@ -1623,19 +1869,24 @@ export class MessageHandler {
|
|
|
1623
1869
|
const repos = [];
|
|
1624
1870
|
const seen = new Set();
|
|
1625
1871
|
try {
|
|
1626
|
-
// 1. Check if cwd itself is a git repo
|
|
1627
|
-
|
|
1628
|
-
|
|
1872
|
+
// 1. Check if cwd itself is a git repo. Use the cheap fs check rather
|
|
1873
|
+
// than spawning `git branch -a` so a transient git failure (EAGAIN
|
|
1874
|
+
// under daemon load, refs being rewritten by a concurrent fetch/gc,
|
|
1875
|
+
// detached HEAD returning empty `current`) doesn't drop the root
|
|
1876
|
+
// and make the PWA's Git section flash empty. Branch + dirty are
|
|
1877
|
+
// filled in best-effort below; undefined just means "unknown".
|
|
1878
|
+
const isRootRepo = existsSync(join(cwd, '.git'));
|
|
1879
|
+
if (isRootRepo) {
|
|
1629
1880
|
repos.push({
|
|
1630
1881
|
path: cwd,
|
|
1631
1882
|
name: basename(cwd),
|
|
1632
|
-
currentBranch:
|
|
1883
|
+
currentBranch: (await this.getGitBranchQuiet(cwd)) || undefined,
|
|
1633
1884
|
hasChanges: await this.getGitDirtyQuiet(cwd),
|
|
1634
1885
|
});
|
|
1635
1886
|
seen.add(cwd);
|
|
1636
1887
|
}
|
|
1637
1888
|
// 2. Find submodules via git
|
|
1638
|
-
if (
|
|
1889
|
+
if (isRootRepo) {
|
|
1639
1890
|
try {
|
|
1640
1891
|
const git = new GitOperations(cwd);
|
|
1641
1892
|
const submodules = await git.getSubmodules();
|
|
@@ -1725,10 +1976,9 @@ export class MessageHandler {
|
|
|
1725
1976
|
handleDeleteProject(message) {
|
|
1726
1977
|
const { cwd } = message.payload;
|
|
1727
1978
|
const registry = getSessionRegistry();
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
1979
|
+
const liveHere = this.claudeService.getActiveSessions().filter((s) => s.cwd === cwd);
|
|
1980
|
+
for (const s of liveHere) {
|
|
1981
|
+
this.claudeService.closeSession(s.sessionId);
|
|
1732
1982
|
}
|
|
1733
1983
|
const active = registry.getEntriesForProject(cwd);
|
|
1734
1984
|
let archivedCount = 0;
|
|
@@ -1739,10 +1989,12 @@ export class MessageHandler {
|
|
|
1739
1989
|
this.onHistoryUpdated?.(cwd, updated, 'upsert');
|
|
1740
1990
|
}
|
|
1741
1991
|
}
|
|
1742
|
-
|
|
1992
|
+
const hadCodingPath = this.codingPaths.has(cwd);
|
|
1993
|
+
if (hadCodingPath) {
|
|
1743
1994
|
this.codingPaths.delete(cwd);
|
|
1744
1995
|
removeManagedCodingPath(cwd);
|
|
1745
1996
|
}
|
|
1997
|
+
console.log(`[project:delete] cwd=${cwd} closedSessions=${liveHere.length} archived=${archivedCount} removedCodingPath=${hadCodingPath}`);
|
|
1746
1998
|
const response = createMessage('project:delete:response', { success: true, archivedCount });
|
|
1747
1999
|
response.id = message.id;
|
|
1748
2000
|
return response;
|
|
@@ -1787,6 +2039,100 @@ export class MessageHandler {
|
|
|
1787
2039
|
response.id = message.id;
|
|
1788
2040
|
return response;
|
|
1789
2041
|
}
|
|
2042
|
+
// ── Terminal (PTY) handlers ──────────────────────────────────────────────
|
|
2043
|
+
async handleTerminalCreate(message) {
|
|
2044
|
+
let payload;
|
|
2045
|
+
try {
|
|
2046
|
+
const summary = await getTerminalManager().create(message.payload);
|
|
2047
|
+
payload = { success: true, terminal: summary };
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
payload = {
|
|
2051
|
+
success: false,
|
|
2052
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const response = createMessage('terminal:create:response', payload);
|
|
2056
|
+
response.id = message.id;
|
|
2057
|
+
return response;
|
|
2058
|
+
}
|
|
2059
|
+
async handleTerminalInput(message) {
|
|
2060
|
+
let payload;
|
|
2061
|
+
try {
|
|
2062
|
+
getTerminalManager().write(message.payload.terminalId, message.payload.data);
|
|
2063
|
+
payload = { success: true };
|
|
2064
|
+
}
|
|
2065
|
+
catch (err) {
|
|
2066
|
+
payload = {
|
|
2067
|
+
success: false,
|
|
2068
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
const response = createMessage('terminal:input:response', payload);
|
|
2072
|
+
response.id = message.id;
|
|
2073
|
+
return response;
|
|
2074
|
+
}
|
|
2075
|
+
async handleTerminalResize(message) {
|
|
2076
|
+
let payload;
|
|
2077
|
+
try {
|
|
2078
|
+
getTerminalManager().resize(message.payload.terminalId, message.payload.cols, message.payload.rows);
|
|
2079
|
+
payload = { success: true };
|
|
2080
|
+
}
|
|
2081
|
+
catch (err) {
|
|
2082
|
+
payload = {
|
|
2083
|
+
success: false,
|
|
2084
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
const response = createMessage('terminal:resize:response', payload);
|
|
2088
|
+
response.id = message.id;
|
|
2089
|
+
return response;
|
|
2090
|
+
}
|
|
2091
|
+
async handleTerminalClose(message) {
|
|
2092
|
+
let payload;
|
|
2093
|
+
try {
|
|
2094
|
+
getTerminalManager().close(message.payload.terminalId, message.payload.force ?? false);
|
|
2095
|
+
payload = { success: true };
|
|
2096
|
+
}
|
|
2097
|
+
catch (err) {
|
|
2098
|
+
payload = {
|
|
2099
|
+
success: false,
|
|
2100
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
const response = createMessage('terminal:close:response', payload);
|
|
2104
|
+
response.id = message.id;
|
|
2105
|
+
return response;
|
|
2106
|
+
}
|
|
2107
|
+
async handleTerminalRename(message) {
|
|
2108
|
+
let payload;
|
|
2109
|
+
try {
|
|
2110
|
+
const summary = getTerminalManager().rename(message.payload.terminalId, message.payload.title);
|
|
2111
|
+
payload = { success: true, terminal: summary };
|
|
2112
|
+
}
|
|
2113
|
+
catch (err) {
|
|
2114
|
+
payload = {
|
|
2115
|
+
success: false,
|
|
2116
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
const response = createMessage('terminal:rename:response', payload);
|
|
2120
|
+
response.id = message.id;
|
|
2121
|
+
return response;
|
|
2122
|
+
}
|
|
2123
|
+
// ── File browser handlers ────────────────────────────────────────────────
|
|
2124
|
+
async handleFilesList(message) {
|
|
2125
|
+
const payload = await getFileBrowser().list(message.payload);
|
|
2126
|
+
const response = createMessage('files:list:response', payload);
|
|
2127
|
+
response.id = message.id;
|
|
2128
|
+
return response;
|
|
2129
|
+
}
|
|
2130
|
+
async handleFilesRead(message) {
|
|
2131
|
+
const payload = await getFileBrowser().read(message.payload);
|
|
2132
|
+
const response = createMessage('files:read:response', payload);
|
|
2133
|
+
response.id = message.id;
|
|
2134
|
+
return response;
|
|
2135
|
+
}
|
|
1790
2136
|
createErrorResponse(id, code, message) {
|
|
1791
2137
|
const response = createMessage('error', { code, message });
|
|
1792
2138
|
response.id = id;
|