@sumicom/quicksave 0.6.3 → 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 +69 -0
- 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 +136 -19
- 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/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 +141 -30
- package/dist/ai/sessionManager.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -5
- package/dist/config.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +32 -0
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +386 -164
- 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/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 +5 -2
|
@@ -2,10 +2,13 @@ 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';
|
|
10
13
|
import { existsSync } from 'fs';
|
|
11
14
|
import { join, dirname, basename } from 'path';
|
|
@@ -14,17 +17,22 @@ const VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
|
14
17
|
const CODEX_MODELS_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours
|
|
15
18
|
export class MessageHandler {
|
|
16
19
|
repos;
|
|
17
|
-
agentVersion = '0.6.
|
|
20
|
+
agentVersion = '0.6.4';
|
|
18
21
|
defaultRepoPath;
|
|
19
22
|
clientRepos = new Map(); // peerAddress -> repoPath
|
|
20
23
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
21
24
|
availableRepos;
|
|
22
25
|
codingPaths = new Map(); // path -> CodingPath
|
|
23
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();
|
|
24
31
|
claudeService = new SessionManager([
|
|
25
32
|
new ClaudeCodeProvider(),
|
|
26
33
|
new CodexSdkProvider(),
|
|
27
34
|
]);
|
|
35
|
+
pushClient = null;
|
|
28
36
|
latestVersionCache = null;
|
|
29
37
|
versionCheckInFlight = null;
|
|
30
38
|
codexModelsCache = null;
|
|
@@ -193,9 +201,22 @@ export class MessageHandler {
|
|
|
193
201
|
getActiveSessionCount() {
|
|
194
202
|
return this.claudeService.getActiveSessionCount();
|
|
195
203
|
}
|
|
204
|
+
/** Exposed so the daemon can wire state-updated events to broadcasts. */
|
|
205
|
+
getCommitSummaryStore() {
|
|
206
|
+
return this.commitSummaryStore;
|
|
207
|
+
}
|
|
196
208
|
getClaudeService() {
|
|
197
209
|
return this.claudeService;
|
|
198
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
|
+
}
|
|
199
220
|
getAiService() {
|
|
200
221
|
const apiKey = getAnthropicApiKey();
|
|
201
222
|
if (!apiKey)
|
|
@@ -211,129 +232,155 @@ export class MessageHandler {
|
|
|
211
232
|
if (isVerbose) {
|
|
212
233
|
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
213
234
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return this.handleUnstage(message, peerAddress);
|
|
228
|
-
case 'git:stage-patch':
|
|
229
|
-
return this.handleStagePatch(message, peerAddress);
|
|
230
|
-
case 'git:unstage-patch':
|
|
231
|
-
return this.handleUnstagePatch(message, peerAddress);
|
|
232
|
-
case 'git:commit':
|
|
233
|
-
return this.handleCommit(message, peerAddress);
|
|
234
|
-
case 'git:log':
|
|
235
|
-
return this.handleLog(message, peerAddress);
|
|
236
|
-
case 'git:branches':
|
|
237
|
-
return this.handleBranches(peerAddress);
|
|
238
|
-
case 'git:checkout':
|
|
239
|
-
return this.handleCheckout(message, peerAddress);
|
|
240
|
-
case 'git:discard':
|
|
241
|
-
return this.handleDiscard(message, peerAddress);
|
|
242
|
-
case 'git:untrack':
|
|
243
|
-
return this.handleUntrack(message, peerAddress);
|
|
244
|
-
case 'git:submodules':
|
|
245
|
-
return this.handleSubmodules(message, peerAddress);
|
|
246
|
-
case 'git:config-get':
|
|
247
|
-
return this.handleGitConfigGet(message, peerAddress);
|
|
248
|
-
case 'git:config-set':
|
|
249
|
-
return this.handleGitConfigSet(message, peerAddress);
|
|
250
|
-
case 'git:gitignore-add':
|
|
251
|
-
return this.handleGitignoreAdd(message, peerAddress);
|
|
252
|
-
case 'git:gitignore-read':
|
|
253
|
-
return this.handleGitignoreRead(message, peerAddress);
|
|
254
|
-
case 'git:gitignore-write':
|
|
255
|
-
return this.handleGitignoreWrite(message, peerAddress);
|
|
256
|
-
case 'ai:generate-commit-summary':
|
|
257
|
-
return this.handleGenerateCommitSummary(message, peerAddress);
|
|
258
|
-
case 'ai:set-api-key':
|
|
259
|
-
return this.handleSetApiKey(message);
|
|
260
|
-
case 'ai:get-api-key-status':
|
|
261
|
-
return this.handleGetApiKeyStatus(message);
|
|
262
|
-
case 'agent:list-repos':
|
|
263
|
-
return this.handleListRepos(message, peerAddress);
|
|
264
|
-
case 'agent:switch-repo':
|
|
265
|
-
return this.handleSwitchRepo(message, peerAddress);
|
|
266
|
-
case 'agent:browse-directory':
|
|
267
|
-
return this.handleBrowseDirectory(message);
|
|
268
|
-
case 'agent:add-repo':
|
|
269
|
-
return this.handleAddRepo(message);
|
|
270
|
-
case 'agent:remove-repo':
|
|
271
|
-
return this.handleRemoveRepo(message);
|
|
272
|
-
case 'agent:clone-repo':
|
|
273
|
-
return this.handleCloneRepo(message);
|
|
274
|
-
case 'agent:list-coding-paths':
|
|
275
|
-
return this.handleListCodingPaths(message);
|
|
276
|
-
case 'agent:add-coding-path':
|
|
277
|
-
return this.handleAddCodingPath(message);
|
|
278
|
-
case 'agent:remove-coding-path':
|
|
279
|
-
return this.handleRemoveCodingPath(message);
|
|
280
|
-
case 'agent:check-update':
|
|
281
|
-
return this.handleAgentCheckUpdate(message);
|
|
282
|
-
case 'agent:update':
|
|
283
|
-
return this.handleAgentUpdate(message);
|
|
284
|
-
case 'agent:restart':
|
|
285
|
-
return this.handleAgentRestart(message);
|
|
286
|
-
case 'codex:list-models':
|
|
287
|
-
return this.handleCodexListModels(message);
|
|
288
|
-
// Claude Code SDK
|
|
289
|
-
case 'claude:list-sessions':
|
|
290
|
-
return this.handleClaudeListSessions(message, peerAddress);
|
|
291
|
-
case 'claude:start':
|
|
292
|
-
return this.handleClaudeStart(message, peerAddress);
|
|
293
|
-
case 'claude:resume':
|
|
294
|
-
return this.handleClaudeResume(message, peerAddress);
|
|
295
|
-
case 'claude:cancel':
|
|
296
|
-
return this.handleClaudeCancel(message);
|
|
297
|
-
case 'claude:close':
|
|
298
|
-
return this.handleClaudeClose(message);
|
|
299
|
-
case 'claude:user-input-response':
|
|
300
|
-
return this.handleClaudeUserInputResponse(message);
|
|
301
|
-
case 'claude:get-preferences':
|
|
302
|
-
return this.handleGetPreferences(message);
|
|
303
|
-
case 'claude:set-preferences':
|
|
304
|
-
return this.handleSetPreferences(message);
|
|
305
|
-
case 'claude:set-session-permission':
|
|
306
|
-
return this.handleSetSessionPermission(message);
|
|
307
|
-
case 'claude:active-sessions':
|
|
308
|
-
return this.handleGetActiveSessions(message);
|
|
309
|
-
case 'claude:get-cards':
|
|
310
|
-
return this.handleClaudeGetCards(message, peerAddress);
|
|
311
|
-
case 'claude:unsubscribe': {
|
|
312
|
-
const unsub = message.payload;
|
|
313
|
-
this.onPeerUnsubscribed?.(peerAddress, unsub.sessionId);
|
|
314
|
-
return createMessage('claude:unsubscribe:response', { ok: true });
|
|
315
|
-
}
|
|
316
|
-
case 'session:get-config':
|
|
317
|
-
return this.handleGetSessionConfig(message);
|
|
318
|
-
case 'session:set-config':
|
|
319
|
-
return this.handleSetSessionConfig(message);
|
|
320
|
-
case 'session:list-history':
|
|
321
|
-
return this.handleListHistory(message, peerAddress);
|
|
322
|
-
case 'session:update-history':
|
|
323
|
-
return this.handleUpdateHistory(message);
|
|
324
|
-
case 'session:delete-history':
|
|
325
|
-
return this.handleDeleteHistory(message);
|
|
326
|
-
case 'project:list-summaries':
|
|
327
|
-
return this.handleListProjectSummaries(message);
|
|
328
|
-
case 'project:list-repos':
|
|
329
|
-
return await this.handleListProjectRepos(message);
|
|
330
|
-
default:
|
|
331
|
-
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;
|
|
332
248
|
}
|
|
333
249
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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}`);
|
|
337
384
|
}
|
|
338
385
|
}
|
|
339
386
|
handleHandshake(message, peerAddress) {
|
|
@@ -492,6 +539,9 @@ export class MessageHandler {
|
|
|
492
539
|
try {
|
|
493
540
|
const { message: commitMessage, description, attribution } = message.payload;
|
|
494
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);
|
|
495
545
|
const response = createMessage('git:commit:response', {
|
|
496
546
|
success: true,
|
|
497
547
|
hash,
|
|
@@ -711,69 +761,170 @@ export class MessageHandler {
|
|
|
711
761
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
712
762
|
}
|
|
713
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
|
+
*/
|
|
714
770
|
async handleGenerateCommitSummary(message, peerAddress) {
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
}
|
|
724
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.
|
|
725
793
|
try {
|
|
726
|
-
const
|
|
727
|
-
const status = await git.getStatus();
|
|
794
|
+
const status = await this.getGit(peerAddress).getStatus();
|
|
728
795
|
if (status.staged.length === 0) {
|
|
729
|
-
|
|
796
|
+
return respond({
|
|
730
797
|
success: false,
|
|
731
798
|
error: 'No staged changes to summarize',
|
|
732
799
|
errorCode: 'NO_STAGED_CHANGES',
|
|
733
800
|
});
|
|
734
|
-
response.id = message.id;
|
|
735
|
-
return response;
|
|
736
801
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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([
|
|
740
844
|
git.getLog(10).catch(() => []),
|
|
741
845
|
git.getBranches().catch(() => ({ current: '' })),
|
|
742
846
|
git.readCommitConventions().catch(() => undefined),
|
|
743
847
|
]);
|
|
744
848
|
const recentCommits = recentLog.map((c) => c.message);
|
|
745
849
|
const branchName = branchInfo.current;
|
|
746
|
-
|
|
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)));
|
|
747
880
|
const result = await aiService.generateSummary({
|
|
748
881
|
diffs,
|
|
749
|
-
context:
|
|
750
|
-
model:
|
|
882
|
+
context: payload.context,
|
|
883
|
+
model: payload.model,
|
|
751
884
|
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
752
885
|
branchName: branchName || undefined,
|
|
753
886
|
conventions,
|
|
754
|
-
attribution:
|
|
887
|
+
attribution: payload.attribution,
|
|
755
888
|
});
|
|
756
|
-
|
|
757
|
-
success: true,
|
|
889
|
+
this.commitSummaryStore.setResult(repoPath, token, {
|
|
758
890
|
summary: result.summary,
|
|
759
891
|
description: result.description,
|
|
760
892
|
tokenUsage: result.tokenUsage,
|
|
761
893
|
cached: result.cached,
|
|
762
894
|
});
|
|
763
|
-
response.id = message.id;
|
|
764
|
-
return response;
|
|
765
895
|
}
|
|
766
896
|
catch (error) {
|
|
897
|
+
if (error instanceof CommitSummaryCliError) {
|
|
898
|
+
this.commitSummaryStore.setError(repoPath, token, error.message, error.errorCode);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
767
901
|
const errorMessage = error instanceof Error ? error.message : 'Failed to generate summary';
|
|
768
902
|
const isRateLimit = errorMessage.includes('rate_limit');
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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();
|
|
776
926
|
}
|
|
927
|
+
return this.aiCliService;
|
|
777
928
|
}
|
|
778
929
|
handleSetApiKey(message) {
|
|
779
930
|
try {
|
|
@@ -1220,7 +1371,7 @@ export class MessageHandler {
|
|
|
1220
1371
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1221
1372
|
const streamId = generateMessageId();
|
|
1222
1373
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1223
|
-
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]' : ''}`);
|
|
1224
1375
|
try {
|
|
1225
1376
|
const sessionId = await this.claudeService.startSession({
|
|
1226
1377
|
prompt,
|
|
@@ -1237,6 +1388,13 @@ export class MessageHandler {
|
|
|
1237
1388
|
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
1238
1389
|
// Register in session history
|
|
1239
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
|
+
});
|
|
1240
1398
|
const registry = getSessionRegistry();
|
|
1241
1399
|
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1242
1400
|
const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
|
|
@@ -1269,7 +1427,8 @@ export class MessageHandler {
|
|
|
1269
1427
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1270
1428
|
const streamId = generateMessageId();
|
|
1271
1429
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1272
|
-
|
|
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)}`);
|
|
1273
1432
|
// Subscribe before resumeSession starts streaming — hot resume emits events
|
|
1274
1433
|
// immediately (user_message, then assistant turns), so we must be subscribed
|
|
1275
1434
|
// before session.send() fires, not after resumeSession() returns.
|
|
@@ -1287,6 +1446,12 @@ export class MessageHandler {
|
|
|
1287
1446
|
if (actualSessionId !== requestedId) {
|
|
1288
1447
|
this.onPeerSubscribed?.(peerAddress, actualSessionId);
|
|
1289
1448
|
}
|
|
1449
|
+
getEventStore().record({
|
|
1450
|
+
type: 'prompt_sent',
|
|
1451
|
+
sessionId: actualSessionId,
|
|
1452
|
+
cwd,
|
|
1453
|
+
data: { kind: 'resume', promptLength: prompt.length, agent: resolvedAgent },
|
|
1454
|
+
});
|
|
1290
1455
|
// Update session history
|
|
1291
1456
|
const registry = getSessionRegistry();
|
|
1292
1457
|
const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
|
|
@@ -1314,6 +1479,9 @@ export class MessageHandler {
|
|
|
1314
1479
|
async handleClaudeCancel(message) {
|
|
1315
1480
|
const { sessionId } = message.payload;
|
|
1316
1481
|
const success = await this.claudeService.cancelSession(sessionId);
|
|
1482
|
+
if (success) {
|
|
1483
|
+
getEventStore().record({ type: 'session_cancelled', sessionId });
|
|
1484
|
+
}
|
|
1317
1485
|
const response = createMessage('claude:cancel:response', { success, error: success ? undefined : 'Session not found or already ended' });
|
|
1318
1486
|
response.id = message.id;
|
|
1319
1487
|
return response;
|
|
@@ -1366,10 +1534,18 @@ export class MessageHandler {
|
|
|
1366
1534
|
response.id = message.id;
|
|
1367
1535
|
return response;
|
|
1368
1536
|
}
|
|
1369
|
-
handleSetSessionPermission(message) {
|
|
1537
|
+
async handleSetSessionPermission(message) {
|
|
1370
1538
|
const { sessionId, permissionMode } = message.payload;
|
|
1371
|
-
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
1372
|
-
|
|
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
|
+
}
|
|
1373
1549
|
const response = createMessage('claude:set-session-permission:response', { success, sessionId, permissionMode });
|
|
1374
1550
|
response.id = message.id;
|
|
1375
1551
|
return response;
|
|
@@ -1381,12 +1557,43 @@ export class MessageHandler {
|
|
|
1381
1557
|
response.id = message.id;
|
|
1382
1558
|
return response;
|
|
1383
1559
|
}
|
|
1384
|
-
handleSetSessionConfig(message) {
|
|
1560
|
+
async handleSetSessionConfig(message) {
|
|
1385
1561
|
const { sessionId, key, value } = message.payload;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
+
}
|
|
1390
1597
|
}
|
|
1391
1598
|
handleGetActiveSessions(message) {
|
|
1392
1599
|
const sessions = this.claudeService.getActiveSessions();
|
|
@@ -1558,6 +1765,21 @@ export class MessageHandler {
|
|
|
1558
1765
|
return undefined;
|
|
1559
1766
|
}
|
|
1560
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
|
+
}
|
|
1561
1783
|
createErrorResponse(id, code, message) {
|
|
1562
1784
|
const response = createMessage('error', { code, message });
|
|
1563
1785
|
response.id = id;
|