@sumicom/quicksave 0.6.2 → 0.6.4
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/dist/ai/cardBuilder.d.ts +27 -0
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +72 -5
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +1 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -1
- package/dist/ai/claudeCliProvider.js +187 -21
- package/dist/ai/claudeCliProvider.js.map +1 -1
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -1
- package/dist/ai/claudeSdkProvider.js +17 -6
- package/dist/ai/claudeSdkProvider.js.map +1 -1
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +37 -9
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/commitSummaryCli.d.ts +67 -0
- package/dist/ai/commitSummaryCli.d.ts.map +1 -0
- package/dist/ai/commitSummaryCli.js +416 -0
- package/dist/ai/commitSummaryCli.js.map +1 -0
- package/dist/ai/commitSummaryStore.d.ts +53 -0
- package/dist/ai/commitSummaryStore.d.ts.map +1 -0
- package/dist/ai/commitSummaryStore.js +136 -0
- package/dist/ai/commitSummaryStore.js.map +1 -0
- package/dist/ai/debugLogger.d.ts.map +1 -1
- package/dist/ai/debugLogger.js +3 -4
- package/dist/ai/debugLogger.js.map +1 -1
- package/dist/ai/provider.d.ts +7 -2
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sessionManager.d.ts +14 -2
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +143 -31
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/ai/sessionRegistry.d.ts +2 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -1
- package/dist/ai/sessionRegistry.js +4 -0
- package/dist/ai/sessionRegistry.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -15
- package/dist/config.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +34 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +513 -161
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/pushClient.d.ts +69 -0
- package/dist/service/pushClient.d.ts.map +1 -0
- package/dist/service/pushClient.js +119 -0
- package/dist/service/pushClient.js.map +1 -0
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +102 -15
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +18 -1
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +43 -18
- package/dist/service/singleton.js.map +1 -1
- package/dist/service/types.d.ts +1 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service/types.js +1 -1
- package/dist/service/types.js.map +1 -1
- package/dist/storage/eventStore.d.ts +65 -0
- package/dist/storage/eventStore.d.ts.map +1 -0
- package/dist/storage/eventStore.js +122 -0
- package/dist/storage/eventStore.js.map +1 -0
- package/package.json +6 -3
|
@@ -2,28 +2,37 @@ import { createMessage, generateMessageId, } from '@sumicom/quicksave-shared';
|
|
|
2
2
|
import { GitOperations } from '../git/operations.js';
|
|
3
3
|
import { getAnthropicApiKey, setAnthropicApiKey, hasAnthropicApiKey, addManagedRepo, removeManagedRepo, addManagedCodingPath, removeManagedCodingPath } from '../config.js';
|
|
4
4
|
import { CommitSummaryService } from '../ai/commitSummary.js';
|
|
5
|
+
import { CommitSummaryCliService, CommitSummaryCliError } from '../ai/commitSummaryCli.js';
|
|
6
|
+
import { CommitSummaryStateStore } from '../ai/commitSummaryStore.js';
|
|
5
7
|
import { SessionManager } from '../ai/sessionManager.js';
|
|
6
8
|
import { ClaudeCodeProvider } from '../ai/claudeCodeProvider.js';
|
|
7
9
|
import { CodexSdkProvider } from '../ai/codexSdkProvider.js';
|
|
8
10
|
import { getSessionRegistry } from '../ai/sessionRegistry.js';
|
|
11
|
+
import { getEventStore } from '../storage/eventStore.js';
|
|
9
12
|
import { readdir, stat, readFile } from 'fs/promises';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
10
14
|
import { join, dirname, basename } from 'path';
|
|
11
15
|
import { homedir } from 'os';
|
|
12
16
|
const VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
13
17
|
const CODEX_MODELS_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
14
18
|
export class MessageHandler {
|
|
15
19
|
repos;
|
|
16
|
-
agentVersion = '0.6.
|
|
20
|
+
agentVersion = '0.6.4';
|
|
17
21
|
defaultRepoPath;
|
|
18
22
|
clientRepos = new Map(); // peerAddress -> repoPath
|
|
19
23
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
20
24
|
availableRepos;
|
|
21
25
|
codingPaths = new Map(); // path -> CodingPath
|
|
22
26
|
aiService = null;
|
|
27
|
+
aiCliService = null;
|
|
28
|
+
/** Per-repo agent-owned commit summary state. The daemon wires the
|
|
29
|
+
* `state-updated` event to `connection.broadcast` (see service/run.ts). */
|
|
30
|
+
commitSummaryStore = new CommitSummaryStateStore();
|
|
23
31
|
claudeService = new SessionManager([
|
|
24
32
|
new ClaudeCodeProvider(),
|
|
25
33
|
new CodexSdkProvider(),
|
|
26
34
|
]);
|
|
35
|
+
pushClient = null;
|
|
27
36
|
latestVersionCache = null;
|
|
28
37
|
versionCheckInFlight = null;
|
|
29
38
|
codexModelsCache = null;
|
|
@@ -192,9 +201,22 @@ export class MessageHandler {
|
|
|
192
201
|
getActiveSessionCount() {
|
|
193
202
|
return this.claudeService.getActiveSessionCount();
|
|
194
203
|
}
|
|
204
|
+
/** Exposed so the daemon can wire state-updated events to broadcasts. */
|
|
205
|
+
getCommitSummaryStore() {
|
|
206
|
+
return this.commitSummaryStore;
|
|
207
|
+
}
|
|
195
208
|
getClaudeService() {
|
|
196
209
|
return this.claudeService;
|
|
197
210
|
}
|
|
211
|
+
/** Daemon wires this after construction so push:subscription-offer can
|
|
212
|
+
* forward to the relay. Absent in tests or when VAPID/signaling aren't
|
|
213
|
+
* configured — offer responses will report an error in that case. */
|
|
214
|
+
setPushClient(client) {
|
|
215
|
+
this.pushClient = client;
|
|
216
|
+
}
|
|
217
|
+
getPushClient() {
|
|
218
|
+
return this.pushClient;
|
|
219
|
+
}
|
|
198
220
|
getAiService() {
|
|
199
221
|
const apiKey = getAnthropicApiKey();
|
|
200
222
|
if (!apiKey)
|
|
@@ -210,125 +232,155 @@ export class MessageHandler {
|
|
|
210
232
|
if (isVerbose) {
|
|
211
233
|
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
212
234
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return this.handleUnstage(message, peerAddress);
|
|
227
|
-
case 'git:stage-patch':
|
|
228
|
-
return this.handleStagePatch(message, peerAddress);
|
|
229
|
-
case 'git:unstage-patch':
|
|
230
|
-
return this.handleUnstagePatch(message, peerAddress);
|
|
231
|
-
case 'git:commit':
|
|
232
|
-
return this.handleCommit(message, peerAddress);
|
|
233
|
-
case 'git:log':
|
|
234
|
-
return this.handleLog(message, peerAddress);
|
|
235
|
-
case 'git:branches':
|
|
236
|
-
return this.handleBranches(peerAddress);
|
|
237
|
-
case 'git:checkout':
|
|
238
|
-
return this.handleCheckout(message, peerAddress);
|
|
239
|
-
case 'git:discard':
|
|
240
|
-
return this.handleDiscard(message, peerAddress);
|
|
241
|
-
case 'git:untrack':
|
|
242
|
-
return this.handleUntrack(message, peerAddress);
|
|
243
|
-
case 'git:submodules':
|
|
244
|
-
return this.handleSubmodules(message, peerAddress);
|
|
245
|
-
case 'git:config-get':
|
|
246
|
-
return this.handleGitConfigGet(message, peerAddress);
|
|
247
|
-
case 'git:config-set':
|
|
248
|
-
return this.handleGitConfigSet(message, peerAddress);
|
|
249
|
-
case 'git:gitignore-add':
|
|
250
|
-
return this.handleGitignoreAdd(message, peerAddress);
|
|
251
|
-
case 'git:gitignore-read':
|
|
252
|
-
return this.handleGitignoreRead(message, peerAddress);
|
|
253
|
-
case 'git:gitignore-write':
|
|
254
|
-
return this.handleGitignoreWrite(message, peerAddress);
|
|
255
|
-
case 'ai:generate-commit-summary':
|
|
256
|
-
return this.handleGenerateCommitSummary(message, peerAddress);
|
|
257
|
-
case 'ai:set-api-key':
|
|
258
|
-
return this.handleSetApiKey(message);
|
|
259
|
-
case 'ai:get-api-key-status':
|
|
260
|
-
return this.handleGetApiKeyStatus(message);
|
|
261
|
-
case 'agent:list-repos':
|
|
262
|
-
return this.handleListRepos(message, peerAddress);
|
|
263
|
-
case 'agent:switch-repo':
|
|
264
|
-
return this.handleSwitchRepo(message, peerAddress);
|
|
265
|
-
case 'agent:browse-directory':
|
|
266
|
-
return this.handleBrowseDirectory(message);
|
|
267
|
-
case 'agent:add-repo':
|
|
268
|
-
return this.handleAddRepo(message);
|
|
269
|
-
case 'agent:remove-repo':
|
|
270
|
-
return this.handleRemoveRepo(message);
|
|
271
|
-
case 'agent:clone-repo':
|
|
272
|
-
return this.handleCloneRepo(message);
|
|
273
|
-
case 'agent:list-coding-paths':
|
|
274
|
-
return this.handleListCodingPaths(message);
|
|
275
|
-
case 'agent:add-coding-path':
|
|
276
|
-
return this.handleAddCodingPath(message);
|
|
277
|
-
case 'agent:remove-coding-path':
|
|
278
|
-
return this.handleRemoveCodingPath(message);
|
|
279
|
-
case 'agent:check-update':
|
|
280
|
-
return this.handleAgentCheckUpdate(message);
|
|
281
|
-
case 'agent:update':
|
|
282
|
-
return this.handleAgentUpdate(message);
|
|
283
|
-
case 'agent:restart':
|
|
284
|
-
return this.handleAgentRestart(message);
|
|
285
|
-
case 'codex:list-models':
|
|
286
|
-
return this.handleCodexListModels(message);
|
|
287
|
-
// Claude Code SDK
|
|
288
|
-
case 'claude:list-sessions':
|
|
289
|
-
return this.handleClaudeListSessions(message, peerAddress);
|
|
290
|
-
case 'claude:start':
|
|
291
|
-
return this.handleClaudeStart(message, peerAddress);
|
|
292
|
-
case 'claude:resume':
|
|
293
|
-
return this.handleClaudeResume(message, peerAddress);
|
|
294
|
-
case 'claude:cancel':
|
|
295
|
-
return this.handleClaudeCancel(message);
|
|
296
|
-
case 'claude:close':
|
|
297
|
-
return this.handleClaudeClose(message);
|
|
298
|
-
case 'claude:user-input-response':
|
|
299
|
-
return this.handleClaudeUserInputResponse(message);
|
|
300
|
-
case 'claude:get-preferences':
|
|
301
|
-
return this.handleGetPreferences(message);
|
|
302
|
-
case 'claude:set-preferences':
|
|
303
|
-
return this.handleSetPreferences(message);
|
|
304
|
-
case 'claude:set-session-permission':
|
|
305
|
-
return this.handleSetSessionPermission(message);
|
|
306
|
-
case 'claude:active-sessions':
|
|
307
|
-
return this.handleGetActiveSessions(message);
|
|
308
|
-
case 'claude:get-cards':
|
|
309
|
-
return this.handleClaudeGetCards(message, peerAddress);
|
|
310
|
-
case 'claude:unsubscribe': {
|
|
311
|
-
const unsub = message.payload;
|
|
312
|
-
this.onPeerUnsubscribed?.(peerAddress, unsub.sessionId);
|
|
313
|
-
return createMessage('claude:unsubscribe:response', { ok: true });
|
|
314
|
-
}
|
|
315
|
-
case 'session:get-config':
|
|
316
|
-
return this.handleGetSessionConfig(message);
|
|
317
|
-
case 'session:set-config':
|
|
318
|
-
return this.handleSetSessionConfig(message);
|
|
319
|
-
case 'session:list-history':
|
|
320
|
-
return this.handleListHistory(message, peerAddress);
|
|
321
|
-
case 'session:update-history':
|
|
322
|
-
return this.handleUpdateHistory(message);
|
|
323
|
-
case 'session:delete-history':
|
|
324
|
-
return this.handleDeleteHistory(message);
|
|
325
|
-
default:
|
|
326
|
-
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
235
|
+
// Repo-scoped guard for git:* requests. The PWA stamps `repoPath` on
|
|
236
|
+
// each request so a response can't be misapplied if the user has since
|
|
237
|
+
// switched repos. If the envelope's repoPath differs from the peer's
|
|
238
|
+
// current repo, reject with REPO_MISMATCH so the client can discard
|
|
239
|
+
// rather than render stale data.
|
|
240
|
+
if (message.type.startsWith('git:') &&
|
|
241
|
+
!message.type.endsWith(':response') &&
|
|
242
|
+
typeof message.repoPath === 'string') {
|
|
243
|
+
const currentRepo = this.getClientRepoPath(peerAddress);
|
|
244
|
+
if (message.repoPath !== currentRepo) {
|
|
245
|
+
const err = this.createErrorResponse(message.id, 'REPO_MISMATCH', `Repo mismatch: request expected ${message.repoPath}, peer is on ${currentRepo}`);
|
|
246
|
+
err.repoPath = currentRepo;
|
|
247
|
+
return err;
|
|
327
248
|
}
|
|
328
249
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
250
|
+
const response = await this.dispatch(message, peerAddress);
|
|
251
|
+
// Stamp git:* responses with the repo the agent actually used so the
|
|
252
|
+
// PWA can validate before applying to the store.
|
|
253
|
+
if (response.type.startsWith('git:') && response.type.endsWith(':response')) {
|
|
254
|
+
response.repoPath = this.getClientRepoPath(peerAddress);
|
|
255
|
+
}
|
|
256
|
+
return response;
|
|
257
|
+
}
|
|
258
|
+
async dispatch(message, peerAddress) {
|
|
259
|
+
switch (message.type) {
|
|
260
|
+
case 'handshake':
|
|
261
|
+
return this.handleHandshake(message, peerAddress);
|
|
262
|
+
case 'ping':
|
|
263
|
+
return createMessage('pong', { timestamp: Date.now() });
|
|
264
|
+
case 'git:status':
|
|
265
|
+
return this.handleStatus(message, peerAddress);
|
|
266
|
+
case 'git:diff':
|
|
267
|
+
return this.handleDiff(message, peerAddress);
|
|
268
|
+
case 'git:stage':
|
|
269
|
+
return this.handleStage(message, peerAddress);
|
|
270
|
+
case 'git:unstage':
|
|
271
|
+
return this.handleUnstage(message, peerAddress);
|
|
272
|
+
case 'git:stage-patch':
|
|
273
|
+
return this.handleStagePatch(message, peerAddress);
|
|
274
|
+
case 'git:unstage-patch':
|
|
275
|
+
return this.handleUnstagePatch(message, peerAddress);
|
|
276
|
+
case 'git:commit':
|
|
277
|
+
return this.handleCommit(message, peerAddress);
|
|
278
|
+
case 'git:log':
|
|
279
|
+
return this.handleLog(message, peerAddress);
|
|
280
|
+
case 'git:branches':
|
|
281
|
+
return this.handleBranches(peerAddress);
|
|
282
|
+
case 'git:checkout':
|
|
283
|
+
return this.handleCheckout(message, peerAddress);
|
|
284
|
+
case 'git:discard':
|
|
285
|
+
return this.handleDiscard(message, peerAddress);
|
|
286
|
+
case 'git:untrack':
|
|
287
|
+
return this.handleUntrack(message, peerAddress);
|
|
288
|
+
case 'git:submodules':
|
|
289
|
+
return this.handleSubmodules(message, peerAddress);
|
|
290
|
+
case 'git:config-get':
|
|
291
|
+
return this.handleGitConfigGet(message, peerAddress);
|
|
292
|
+
case 'git:config-set':
|
|
293
|
+
return this.handleGitConfigSet(message, peerAddress);
|
|
294
|
+
case 'git:gitignore-add':
|
|
295
|
+
return this.handleGitignoreAdd(message, peerAddress);
|
|
296
|
+
case 'git:gitignore-read':
|
|
297
|
+
return this.handleGitignoreRead(message, peerAddress);
|
|
298
|
+
case 'git:gitignore-write':
|
|
299
|
+
return this.handleGitignoreWrite(message, peerAddress);
|
|
300
|
+
case 'ai:generate-commit-summary':
|
|
301
|
+
return this.handleGenerateCommitSummary(message, peerAddress);
|
|
302
|
+
case 'ai:commit-summary:get':
|
|
303
|
+
return this.handleGetCommitSummary(message, peerAddress);
|
|
304
|
+
case 'ai:commit-summary:clear':
|
|
305
|
+
return this.handleClearCommitSummary(message, peerAddress);
|
|
306
|
+
case 'ai:set-api-key':
|
|
307
|
+
return this.handleSetApiKey(message);
|
|
308
|
+
case 'ai:get-api-key-status':
|
|
309
|
+
return this.handleGetApiKeyStatus(message);
|
|
310
|
+
case 'agent:list-repos':
|
|
311
|
+
return this.handleListRepos(message, peerAddress);
|
|
312
|
+
case 'agent:switch-repo':
|
|
313
|
+
return this.handleSwitchRepo(message, peerAddress);
|
|
314
|
+
case 'agent:browse-directory':
|
|
315
|
+
return this.handleBrowseDirectory(message);
|
|
316
|
+
case 'agent:add-repo':
|
|
317
|
+
return this.handleAddRepo(message);
|
|
318
|
+
case 'agent:remove-repo':
|
|
319
|
+
return this.handleRemoveRepo(message);
|
|
320
|
+
case 'agent:clone-repo':
|
|
321
|
+
return this.handleCloneRepo(message);
|
|
322
|
+
case 'agent:list-coding-paths':
|
|
323
|
+
return this.handleListCodingPaths(message);
|
|
324
|
+
case 'agent:add-coding-path':
|
|
325
|
+
return this.handleAddCodingPath(message);
|
|
326
|
+
case 'agent:remove-coding-path':
|
|
327
|
+
return this.handleRemoveCodingPath(message);
|
|
328
|
+
case 'agent:check-update':
|
|
329
|
+
return this.handleAgentCheckUpdate(message);
|
|
330
|
+
case 'agent:update':
|
|
331
|
+
return this.handleAgentUpdate(message);
|
|
332
|
+
case 'agent:restart':
|
|
333
|
+
return this.handleAgentRestart(message);
|
|
334
|
+
case 'codex:list-models':
|
|
335
|
+
return this.handleCodexListModels(message);
|
|
336
|
+
// Claude Code SDK
|
|
337
|
+
case 'claude:list-sessions':
|
|
338
|
+
return this.handleClaudeListSessions(message, peerAddress);
|
|
339
|
+
case 'claude:start':
|
|
340
|
+
return this.handleClaudeStart(message, peerAddress);
|
|
341
|
+
case 'claude:resume':
|
|
342
|
+
return this.handleClaudeResume(message, peerAddress);
|
|
343
|
+
case 'claude:cancel':
|
|
344
|
+
return this.handleClaudeCancel(message);
|
|
345
|
+
case 'claude:close':
|
|
346
|
+
return this.handleClaudeClose(message);
|
|
347
|
+
case 'claude:user-input-response':
|
|
348
|
+
return this.handleClaudeUserInputResponse(message);
|
|
349
|
+
case 'claude:get-preferences':
|
|
350
|
+
return this.handleGetPreferences(message);
|
|
351
|
+
case 'claude:set-preferences':
|
|
352
|
+
return this.handleSetPreferences(message);
|
|
353
|
+
case 'claude:set-session-permission':
|
|
354
|
+
return this.handleSetSessionPermission(message);
|
|
355
|
+
case 'claude:active-sessions':
|
|
356
|
+
return this.handleGetActiveSessions(message);
|
|
357
|
+
case 'claude:get-cards':
|
|
358
|
+
return this.handleClaudeGetCards(message, peerAddress);
|
|
359
|
+
case 'claude:unsubscribe': {
|
|
360
|
+
const unsub = message.payload;
|
|
361
|
+
this.onPeerUnsubscribed?.(peerAddress, unsub.sessionId);
|
|
362
|
+
return createMessage('claude:unsubscribe:response', { ok: true });
|
|
363
|
+
}
|
|
364
|
+
case 'session:get-config':
|
|
365
|
+
return this.handleGetSessionConfig(message);
|
|
366
|
+
case 'session:set-config':
|
|
367
|
+
return this.handleSetSessionConfig(message);
|
|
368
|
+
case 'session:control-request':
|
|
369
|
+
return this.handleControlRequest(message);
|
|
370
|
+
case 'session:list-history':
|
|
371
|
+
return this.handleListHistory(message, peerAddress);
|
|
372
|
+
case 'session:update-history':
|
|
373
|
+
return this.handleUpdateHistory(message);
|
|
374
|
+
case 'session:delete-history':
|
|
375
|
+
return this.handleDeleteHistory(message);
|
|
376
|
+
case 'project:list-summaries':
|
|
377
|
+
return this.handleListProjectSummaries(message);
|
|
378
|
+
case 'project:list-repos':
|
|
379
|
+
return await this.handleListProjectRepos(message);
|
|
380
|
+
case 'push:subscription-offer':
|
|
381
|
+
return this.handlePushSubscriptionOffer(message);
|
|
382
|
+
default:
|
|
383
|
+
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
332
384
|
}
|
|
333
385
|
}
|
|
334
386
|
handleHandshake(message, peerAddress) {
|
|
@@ -487,6 +539,9 @@ export class MessageHandler {
|
|
|
487
539
|
try {
|
|
488
540
|
const { message: commitMessage, description, attribution } = message.payload;
|
|
489
541
|
const hash = await this.getGit(peerAddress).commit(commitMessage, description, attribution ?? true);
|
|
542
|
+
// The pending AI suggestion describes the diff we just committed, so
|
|
543
|
+
// it's stale now — clear it (also broadcasts state → idle to all peers).
|
|
544
|
+
this.commitSummaryStore.clear(repoPath);
|
|
490
545
|
const response = createMessage('git:commit:response', {
|
|
491
546
|
success: true,
|
|
492
547
|
hash,
|
|
@@ -706,69 +761,170 @@ export class MessageHandler {
|
|
|
706
761
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
707
762
|
}
|
|
708
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Kick off AI commit-summary generation. Generation runs asynchronously and
|
|
766
|
+
* streams progress + result via the `ai:commit-summary:updated` broadcast;
|
|
767
|
+
* the request-response just confirms the kickoff (or reports a validation
|
|
768
|
+
* failure that keeps the state idle, e.g. missing API key / no staged changes).
|
|
769
|
+
*/
|
|
709
770
|
async handleGenerateCommitSummary(message, peerAddress) {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
771
|
+
const source = message.payload.source ?? 'api';
|
|
772
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
773
|
+
const respond = (payload) => {
|
|
774
|
+
const r = createMessage('ai:generate-commit-summary:response', payload);
|
|
775
|
+
r.id = message.id;
|
|
776
|
+
return r;
|
|
777
|
+
};
|
|
778
|
+
// API source requires an Anthropic key; CLI source does not.
|
|
779
|
+
let aiService = null;
|
|
780
|
+
if (source === 'api') {
|
|
781
|
+
aiService = this.getAiService();
|
|
782
|
+
if (!aiService) {
|
|
783
|
+
return respond({
|
|
784
|
+
success: false,
|
|
785
|
+
error: 'Configure your API key in Settings',
|
|
786
|
+
errorCode: 'NO_API_KEY',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
719
789
|
}
|
|
790
|
+
// Validate staged changes exist before flipping the state into
|
|
791
|
+
// `generating` — we don't want to show "generating…" just to immediately
|
|
792
|
+
// error out on the same tick.
|
|
720
793
|
try {
|
|
721
|
-
const
|
|
722
|
-
const status = await git.getStatus();
|
|
794
|
+
const status = await this.getGit(peerAddress).getStatus();
|
|
723
795
|
if (status.staged.length === 0) {
|
|
724
|
-
|
|
796
|
+
return respond({
|
|
725
797
|
success: false,
|
|
726
798
|
error: 'No staged changes to summarize',
|
|
727
799
|
errorCode: 'NO_STAGED_CHANGES',
|
|
728
800
|
});
|
|
729
|
-
response.id = message.id;
|
|
730
|
-
return response;
|
|
731
801
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
return respond({
|
|
805
|
+
success: false,
|
|
806
|
+
error: error instanceof Error ? error.message : 'Failed to inspect working tree',
|
|
807
|
+
errorCode: 'API_ERROR',
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
// Register the generation with the state store. The returned token lets
|
|
811
|
+
// us detect supersession (the caller fired off a fresh generation while
|
|
812
|
+
// this one was still running, or the state was cleared).
|
|
813
|
+
let aborted = false;
|
|
814
|
+
let cliChild;
|
|
815
|
+
const token = this.commitSummaryStore.startGenerating(repoPath, source, message.payload.model, () => {
|
|
816
|
+
aborted = true;
|
|
817
|
+
try {
|
|
818
|
+
cliChild?.kill('SIGTERM');
|
|
819
|
+
}
|
|
820
|
+
catch { /* ignore */ }
|
|
821
|
+
});
|
|
822
|
+
// Run generation asynchronously. Errors flow through the state store,
|
|
823
|
+
// NOT through the kickoff response — the response has already been sent.
|
|
824
|
+
void this.runCommitSummary(peerAddress, repoPath, token, source, message.payload, aiService, (child) => {
|
|
825
|
+
cliChild = child;
|
|
826
|
+
if (aborted) {
|
|
827
|
+
try {
|
|
828
|
+
child.kill('SIGTERM');
|
|
829
|
+
}
|
|
830
|
+
catch { /* ignore */ }
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
return respond({ success: true, state: this.commitSummaryStore.get(repoPath) });
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Background worker that actually runs the generation. Reports success,
|
|
837
|
+
* progress, or failure through the CommitSummaryStateStore (which in turn
|
|
838
|
+
* broadcasts `ai:commit-summary:updated` to all peers).
|
|
839
|
+
*/
|
|
840
|
+
async runCommitSummary(peerAddress, repoPath, token, source, payload, aiService, onSpawn) {
|
|
841
|
+
try {
|
|
842
|
+
const git = this.getGit(peerAddress);
|
|
843
|
+
const [recentLog, branchInfo, conventions] = await Promise.all([
|
|
735
844
|
git.getLog(10).catch(() => []),
|
|
736
845
|
git.getBranches().catch(() => ({ current: '' })),
|
|
737
846
|
git.readCommitConventions().catch(() => undefined),
|
|
738
847
|
]);
|
|
739
848
|
const recentCommits = recentLog.map((c) => c.message);
|
|
740
849
|
const branchName = branchInfo.current;
|
|
741
|
-
|
|
850
|
+
if (source === 'claude-cli') {
|
|
851
|
+
const cliService = this.getAiCliService();
|
|
852
|
+
const result = await cliService.generateSummary({
|
|
853
|
+
repoPath,
|
|
854
|
+
context: payload.context,
|
|
855
|
+
model: payload.model,
|
|
856
|
+
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
857
|
+
branchName: branchName || undefined,
|
|
858
|
+
conventions,
|
|
859
|
+
attribution: payload.attribution,
|
|
860
|
+
onProgress: (progress) => {
|
|
861
|
+
this.commitSummaryStore.updateProgress(repoPath, token, progress);
|
|
862
|
+
},
|
|
863
|
+
onSpawn: (child) => {
|
|
864
|
+
onSpawn(child);
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
this.commitSummaryStore.setResult(repoPath, token, {
|
|
868
|
+
summary: result.summary,
|
|
869
|
+
description: result.description,
|
|
870
|
+
tokenUsage: result.tokenUsage,
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
// API path — fetch staged diffs fresh. No per-step progress today;
|
|
875
|
+
// the single Anthropic round-trip is fast enough that phase updates add
|
|
876
|
+
// little value. We still flip to `generating` so the UI shows a spinner.
|
|
877
|
+
this.commitSummaryStore.updateProgress(repoPath, token, { phase: 'generating' });
|
|
878
|
+
const status = await git.getStatus();
|
|
879
|
+
const diffs = await Promise.all(status.staged.map((file) => git.getDiff(file.path, true)));
|
|
742
880
|
const result = await aiService.generateSummary({
|
|
743
881
|
diffs,
|
|
744
|
-
context:
|
|
745
|
-
model:
|
|
882
|
+
context: payload.context,
|
|
883
|
+
model: payload.model,
|
|
746
884
|
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
747
885
|
branchName: branchName || undefined,
|
|
748
886
|
conventions,
|
|
749
|
-
attribution:
|
|
887
|
+
attribution: payload.attribution,
|
|
750
888
|
});
|
|
751
|
-
|
|
752
|
-
success: true,
|
|
889
|
+
this.commitSummaryStore.setResult(repoPath, token, {
|
|
753
890
|
summary: result.summary,
|
|
754
891
|
description: result.description,
|
|
755
892
|
tokenUsage: result.tokenUsage,
|
|
756
893
|
cached: result.cached,
|
|
757
894
|
});
|
|
758
|
-
response.id = message.id;
|
|
759
|
-
return response;
|
|
760
895
|
}
|
|
761
896
|
catch (error) {
|
|
897
|
+
if (error instanceof CommitSummaryCliError) {
|
|
898
|
+
this.commitSummaryStore.setError(repoPath, token, error.message, error.errorCode);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
762
901
|
const errorMessage = error instanceof Error ? error.message : 'Failed to generate summary';
|
|
763
902
|
const isRateLimit = errorMessage.includes('rate_limit');
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
903
|
+
this.commitSummaryStore.setError(repoPath, token, errorMessage, isRateLimit ? 'RATE_LIMITED' : 'API_ERROR');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
handleGetCommitSummary(message, peerAddress) {
|
|
907
|
+
const repoPath = message.payload.repoPath || this.getClientRepoPath(peerAddress);
|
|
908
|
+
const state = this.commitSummaryStore.get(repoPath);
|
|
909
|
+
const response = createMessage('ai:commit-summary:get:response', { state });
|
|
910
|
+
response.id = message.id;
|
|
911
|
+
return response;
|
|
912
|
+
}
|
|
913
|
+
handleClearCommitSummary(message, peerAddress) {
|
|
914
|
+
const repoPath = message.payload.repoPath || this.getClientRepoPath(peerAddress);
|
|
915
|
+
const state = this.commitSummaryStore.clear(repoPath);
|
|
916
|
+
const response = createMessage('ai:commit-summary:clear:response', {
|
|
917
|
+
success: true,
|
|
918
|
+
state,
|
|
919
|
+
});
|
|
920
|
+
response.id = message.id;
|
|
921
|
+
return response;
|
|
922
|
+
}
|
|
923
|
+
getAiCliService() {
|
|
924
|
+
if (!this.aiCliService) {
|
|
925
|
+
this.aiCliService = new CommitSummaryCliService();
|
|
771
926
|
}
|
|
927
|
+
return this.aiCliService;
|
|
772
928
|
}
|
|
773
929
|
handleSetApiKey(message) {
|
|
774
930
|
try {
|
|
@@ -1215,7 +1371,7 @@ export class MessageHandler {
|
|
|
1215
1371
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1216
1372
|
const streamId = generateMessageId();
|
|
1217
1373
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1218
|
-
console.log(`[agent:start] agent=${resolvedAgent ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
|
|
1374
|
+
console.log(`[agent:start] agent=${resolvedAgent ?? 'default'} model=${model ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
|
|
1219
1375
|
try {
|
|
1220
1376
|
const sessionId = await this.claudeService.startSession({
|
|
1221
1377
|
prompt,
|
|
@@ -1232,6 +1388,13 @@ export class MessageHandler {
|
|
|
1232
1388
|
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
1233
1389
|
// Register in session history
|
|
1234
1390
|
const now = Date.now();
|
|
1391
|
+
getEventStore().record({
|
|
1392
|
+
type: 'prompt_sent',
|
|
1393
|
+
sessionId,
|
|
1394
|
+
cwd,
|
|
1395
|
+
time: now,
|
|
1396
|
+
data: { kind: 'start', promptLength: prompt.length, model, agent: resolvedAgent },
|
|
1397
|
+
});
|
|
1235
1398
|
const registry = getSessionRegistry();
|
|
1236
1399
|
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1237
1400
|
const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
|
|
@@ -1264,7 +1427,8 @@ export class MessageHandler {
|
|
|
1264
1427
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1265
1428
|
const streamId = generateMessageId();
|
|
1266
1429
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1267
|
-
|
|
1430
|
+
const activeCfg = this.claudeService.getSessionConfig(requestedId);
|
|
1431
|
+
console.log(`[agent:resume] session=${requestedId} agent=${resolvedAgent ?? 'stored'} model=${activeCfg.model ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
1268
1432
|
// Subscribe before resumeSession starts streaming — hot resume emits events
|
|
1269
1433
|
// immediately (user_message, then assistant turns), so we must be subscribed
|
|
1270
1434
|
// before session.send() fires, not after resumeSession() returns.
|
|
@@ -1282,6 +1446,12 @@ export class MessageHandler {
|
|
|
1282
1446
|
if (actualSessionId !== requestedId) {
|
|
1283
1447
|
this.onPeerSubscribed?.(peerAddress, actualSessionId);
|
|
1284
1448
|
}
|
|
1449
|
+
getEventStore().record({
|
|
1450
|
+
type: 'prompt_sent',
|
|
1451
|
+
sessionId: actualSessionId,
|
|
1452
|
+
cwd,
|
|
1453
|
+
data: { kind: 'resume', promptLength: prompt.length, agent: resolvedAgent },
|
|
1454
|
+
});
|
|
1285
1455
|
// Update session history
|
|
1286
1456
|
const registry = getSessionRegistry();
|
|
1287
1457
|
const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
|
|
@@ -1309,6 +1479,9 @@ export class MessageHandler {
|
|
|
1309
1479
|
async handleClaudeCancel(message) {
|
|
1310
1480
|
const { sessionId } = message.payload;
|
|
1311
1481
|
const success = await this.claudeService.cancelSession(sessionId);
|
|
1482
|
+
if (success) {
|
|
1483
|
+
getEventStore().record({ type: 'session_cancelled', sessionId });
|
|
1484
|
+
}
|
|
1312
1485
|
const response = createMessage('claude:cancel:response', { success, error: success ? undefined : 'Session not found or already ended' });
|
|
1313
1486
|
response.id = message.id;
|
|
1314
1487
|
return response;
|
|
@@ -1361,10 +1534,18 @@ export class MessageHandler {
|
|
|
1361
1534
|
response.id = message.id;
|
|
1362
1535
|
return response;
|
|
1363
1536
|
}
|
|
1364
|
-
handleSetSessionPermission(message) {
|
|
1537
|
+
async handleSetSessionPermission(message) {
|
|
1365
1538
|
const { sessionId, permissionMode } = message.payload;
|
|
1366
|
-
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
1367
|
-
|
|
1539
|
+
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'auto'];
|
|
1540
|
+
let success = false;
|
|
1541
|
+
if (validModes.includes(permissionMode)) {
|
|
1542
|
+
try {
|
|
1543
|
+
success = await this.claudeService.setPermissionLevel(sessionId, permissionMode);
|
|
1544
|
+
}
|
|
1545
|
+
catch {
|
|
1546
|
+
success = false;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1368
1549
|
const response = createMessage('claude:set-session-permission:response', { success, sessionId, permissionMode });
|
|
1369
1550
|
response.id = message.id;
|
|
1370
1551
|
return response;
|
|
@@ -1376,12 +1557,43 @@ export class MessageHandler {
|
|
|
1376
1557
|
response.id = message.id;
|
|
1377
1558
|
return response;
|
|
1378
1559
|
}
|
|
1379
|
-
handleSetSessionConfig(message) {
|
|
1560
|
+
async handleSetSessionConfig(message) {
|
|
1380
1561
|
const { sessionId, key, value } = message.payload;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1562
|
+
console.log(`[agent:set-config] session=${sessionId.slice(0, 8)} ${key}=${String(value)}`);
|
|
1563
|
+
try {
|
|
1564
|
+
const config = await this.claudeService.setSessionConfig(sessionId, key, value);
|
|
1565
|
+
const response = createMessage('session:set-config:response', { success: true, sessionId, config });
|
|
1566
|
+
response.id = message.id;
|
|
1567
|
+
return response;
|
|
1568
|
+
}
|
|
1569
|
+
catch (err) {
|
|
1570
|
+
const config = this.claudeService.getSessionConfig(sessionId);
|
|
1571
|
+
const response = createMessage('session:set-config:response', {
|
|
1572
|
+
success: false,
|
|
1573
|
+
sessionId,
|
|
1574
|
+
config,
|
|
1575
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1576
|
+
});
|
|
1577
|
+
response.id = message.id;
|
|
1578
|
+
return response;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async handleControlRequest(message) {
|
|
1582
|
+
const { sessionId, subtype, params } = message.payload;
|
|
1583
|
+
console.log(`[agent:control-request] session=${sessionId.slice(0, 8)} subtype=${subtype} params=${JSON.stringify(params ?? {})}`);
|
|
1584
|
+
try {
|
|
1585
|
+
const result = await this.claudeService.sendControlRequest(sessionId, subtype, params);
|
|
1586
|
+
const response = createMessage('session:control-request:response', { success: true, sessionId, response: result });
|
|
1587
|
+
response.id = message.id;
|
|
1588
|
+
return response;
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1592
|
+
console.error(`[agent:control-request] error:`, errMsg);
|
|
1593
|
+
const response = createMessage('session:control-request:response', { success: false, sessionId, error: errMsg });
|
|
1594
|
+
response.id = message.id;
|
|
1595
|
+
return response;
|
|
1596
|
+
}
|
|
1385
1597
|
}
|
|
1386
1598
|
handleGetActiveSessions(message) {
|
|
1387
1599
|
const sessions = this.claudeService.getActiveSessions();
|
|
@@ -1392,7 +1604,8 @@ export class MessageHandler {
|
|
|
1392
1604
|
// ── Session Registry (History) ──────────────────────────────────────
|
|
1393
1605
|
handleListHistory(message, peerAddress) {
|
|
1394
1606
|
const cwd = message.payload.cwd || this.getClientRepoPath(peerAddress);
|
|
1395
|
-
const entries = getSessionRegistry().getEntriesForProject(cwd)
|
|
1607
|
+
const entries = getSessionRegistry().getEntriesForProject(cwd)
|
|
1608
|
+
.filter(e => !e.archived);
|
|
1396
1609
|
const response = createMessage('session:list-history:response', { entries });
|
|
1397
1610
|
response.id = message.id;
|
|
1398
1611
|
return response;
|
|
@@ -1418,6 +1631,130 @@ export class MessageHandler {
|
|
|
1418
1631
|
}
|
|
1419
1632
|
return response;
|
|
1420
1633
|
}
|
|
1634
|
+
handleListProjectSummaries(message) {
|
|
1635
|
+
const allEntries = getSessionRegistry().getEntriesForProject()
|
|
1636
|
+
.filter(e => !e.archived);
|
|
1637
|
+
// Group by cwd
|
|
1638
|
+
const byCwd = new Map();
|
|
1639
|
+
for (const entry of allEntries) {
|
|
1640
|
+
let group = byCwd.get(entry.cwd);
|
|
1641
|
+
if (!group) {
|
|
1642
|
+
group = [];
|
|
1643
|
+
byCwd.set(entry.cwd, group);
|
|
1644
|
+
}
|
|
1645
|
+
group.push(entry);
|
|
1646
|
+
}
|
|
1647
|
+
// Build active session set from session manager
|
|
1648
|
+
const activeSessions = this.claudeService.getActiveSessions();
|
|
1649
|
+
const activeCwds = new Set();
|
|
1650
|
+
for (const s of activeSessions) {
|
|
1651
|
+
activeCwds.add(s.cwd);
|
|
1652
|
+
}
|
|
1653
|
+
const projects = [];
|
|
1654
|
+
for (const [cwd, entries] of byCwd) {
|
|
1655
|
+
// entries are already sorted by lastAccessedAt desc from getEntriesForProject
|
|
1656
|
+
const latest = entries[0];
|
|
1657
|
+
projects.push({
|
|
1658
|
+
cwd,
|
|
1659
|
+
sessionCount: entries.length,
|
|
1660
|
+
lastActivityAt: latest.lastAccessedAt,
|
|
1661
|
+
lastSessionTitle: latest.title ?? latest.firstPrompt?.slice(0, 100),
|
|
1662
|
+
hasActiveSession: activeCwds.has(cwd),
|
|
1663
|
+
isGitRepo: existsSync(join(cwd, '.git')),
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
// Sort by lastActivityAt desc
|
|
1667
|
+
projects.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
1668
|
+
const response = createMessage('project:list-summaries:response', { projects });
|
|
1669
|
+
response.id = message.id;
|
|
1670
|
+
return response;
|
|
1671
|
+
}
|
|
1672
|
+
async handleListProjectRepos(message) {
|
|
1673
|
+
const { cwd } = message.payload;
|
|
1674
|
+
const repos = [];
|
|
1675
|
+
const seen = new Set();
|
|
1676
|
+
try {
|
|
1677
|
+
// 1. Check if cwd itself is a git repo
|
|
1678
|
+
const rootBranch = await this.getGitBranchQuiet(cwd);
|
|
1679
|
+
if (rootBranch !== undefined) {
|
|
1680
|
+
repos.push({
|
|
1681
|
+
path: cwd,
|
|
1682
|
+
name: basename(cwd),
|
|
1683
|
+
currentBranch: rootBranch || undefined,
|
|
1684
|
+
});
|
|
1685
|
+
seen.add(cwd);
|
|
1686
|
+
}
|
|
1687
|
+
// 2. Find submodules via git
|
|
1688
|
+
if (rootBranch !== undefined) {
|
|
1689
|
+
try {
|
|
1690
|
+
const git = new GitOperations(cwd);
|
|
1691
|
+
const submodules = await git.getSubmodules();
|
|
1692
|
+
for (const sub of submodules) {
|
|
1693
|
+
const subPath = join(cwd, sub.path);
|
|
1694
|
+
if (seen.has(subPath))
|
|
1695
|
+
continue;
|
|
1696
|
+
seen.add(subPath);
|
|
1697
|
+
const branch = await this.getGitBranchQuiet(subPath);
|
|
1698
|
+
repos.push({
|
|
1699
|
+
path: subPath,
|
|
1700
|
+
name: sub.path,
|
|
1701
|
+
currentBranch: branch || undefined,
|
|
1702
|
+
isSubmodule: true,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// git submodule may fail — ignore
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
// 3. Scan for nested git repos (depth-limited, skips node_modules etc.)
|
|
1711
|
+
const MAX_DEPTH = 3;
|
|
1712
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'vendor', 'dist', 'build', '.next', '__pycache__']);
|
|
1713
|
+
const scan = async (dir, depth) => {
|
|
1714
|
+
if (depth > MAX_DEPTH)
|
|
1715
|
+
return;
|
|
1716
|
+
let names;
|
|
1717
|
+
try {
|
|
1718
|
+
names = await readdir(dir);
|
|
1719
|
+
}
|
|
1720
|
+
catch {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
for (const name of names) {
|
|
1724
|
+
if (name.startsWith('.') || SKIP_DIRS.has(name))
|
|
1725
|
+
continue;
|
|
1726
|
+
const full = join(dir, name);
|
|
1727
|
+
try {
|
|
1728
|
+
const s = await stat(full);
|
|
1729
|
+
if (!s.isDirectory())
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
catch {
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (existsSync(join(full, '.git')) && !seen.has(full)) {
|
|
1736
|
+
seen.add(full);
|
|
1737
|
+
const branch = await this.getGitBranchQuiet(full);
|
|
1738
|
+
repos.push({
|
|
1739
|
+
path: full,
|
|
1740
|
+
name: full.slice(cwd.length + 1),
|
|
1741
|
+
currentBranch: branch || undefined,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
await scan(full, depth + 1);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
await scan(cwd, 0);
|
|
1748
|
+
}
|
|
1749
|
+
catch (error) {
|
|
1750
|
+
const response = createMessage('project:list-repos:response', { repos: [], error: error instanceof Error ? error.message : 'Failed to scan repos' });
|
|
1751
|
+
response.id = message.id;
|
|
1752
|
+
return response;
|
|
1753
|
+
}
|
|
1754
|
+
const response = createMessage('project:list-repos:response', { repos });
|
|
1755
|
+
response.id = message.id;
|
|
1756
|
+
return response;
|
|
1757
|
+
}
|
|
1421
1758
|
async getGitBranchQuiet(cwd) {
|
|
1422
1759
|
try {
|
|
1423
1760
|
const git = new GitOperations(cwd);
|
|
@@ -1428,6 +1765,21 @@ export class MessageHandler {
|
|
|
1428
1765
|
return undefined;
|
|
1429
1766
|
}
|
|
1430
1767
|
}
|
|
1768
|
+
async handlePushSubscriptionOffer(message) {
|
|
1769
|
+
const client = this.pushClient;
|
|
1770
|
+
let payload;
|
|
1771
|
+
if (!client) {
|
|
1772
|
+
payload = { success: false, error: 'push-not-configured' };
|
|
1773
|
+
}
|
|
1774
|
+
else {
|
|
1775
|
+
const { subscription, relayHttpUrl } = message.payload;
|
|
1776
|
+
const result = await client.register(subscription, relayHttpUrl);
|
|
1777
|
+
payload = result.ok ? { success: true } : { success: false, error: result.error ?? `http-${result.status}` };
|
|
1778
|
+
}
|
|
1779
|
+
const response = createMessage('push:subscription-offer:response', payload);
|
|
1780
|
+
response.id = message.id;
|
|
1781
|
+
return response;
|
|
1782
|
+
}
|
|
1431
1783
|
createErrorResponse(id, code, message) {
|
|
1432
1784
|
const response = createMessage('error', { code, message });
|
|
1433
1785
|
response.id = id;
|