@sumicom/quicksave 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +168 -35
- 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 +13 -2
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/sessionManager.d.ts +32 -3
- package/dist/ai/sessionManager.d.ts.map +1 -1
- package/dist/ai/sessionManager.js +229 -36
- 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 +9 -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 +15 -5
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +0 -12
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +1 -44
- package/dist/connection/connection.js.map +1 -1
- package/dist/connection/pubsub.d.ts +5 -5
- package/dist/connection/pubsub.d.ts.map +1 -1
- package/dist/connection/pubsub.js +5 -7
- package/dist/connection/pubsub.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +32 -2
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +395 -176
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/messageBus/busServerTransport.d.ts +29 -0
- package/dist/messageBus/busServerTransport.d.ts.map +1 -0
- package/dist/messageBus/busServerTransport.js +54 -0
- package/dist/messageBus/busServerTransport.js.map +1 -0
- 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 +280 -36
- package/dist/service/run.js.map +1 -1
- package/dist/service/types.d.ts +2 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service/types.js +13 -8
- 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 -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,23 +17,26 @@ 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.
|
|
20
|
+
agentVersion = '0.7.0';
|
|
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;
|
|
31
39
|
codexModelsCheckInFlight = null;
|
|
32
|
-
onPeerSubscribed;
|
|
33
|
-
onPeerUnsubscribed;
|
|
34
40
|
onHistoryUpdated;
|
|
35
41
|
productionBuild;
|
|
36
42
|
constructor(repos, _license, codingPaths, productionBuild = false) {
|
|
@@ -193,9 +199,22 @@ export class MessageHandler {
|
|
|
193
199
|
getActiveSessionCount() {
|
|
194
200
|
return this.claudeService.getActiveSessionCount();
|
|
195
201
|
}
|
|
202
|
+
/** Exposed so the daemon can wire state-updated events to broadcasts. */
|
|
203
|
+
getCommitSummaryStore() {
|
|
204
|
+
return this.commitSummaryStore;
|
|
205
|
+
}
|
|
196
206
|
getClaudeService() {
|
|
197
207
|
return this.claudeService;
|
|
198
208
|
}
|
|
209
|
+
/** Daemon wires this after construction so push:subscription-offer can
|
|
210
|
+
* forward to the relay. Absent in tests or when VAPID/signaling aren't
|
|
211
|
+
* configured — offer responses will report an error in that case. */
|
|
212
|
+
setPushClient(client) {
|
|
213
|
+
this.pushClient = client;
|
|
214
|
+
}
|
|
215
|
+
getPushClient() {
|
|
216
|
+
return this.pushClient;
|
|
217
|
+
}
|
|
199
218
|
getAiService() {
|
|
200
219
|
const apiKey = getAnthropicApiKey();
|
|
201
220
|
if (!apiKey)
|
|
@@ -211,129 +230,150 @@ export class MessageHandler {
|
|
|
211
230
|
if (isVerbose) {
|
|
212
231
|
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
213
232
|
}
|
|
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}`);
|
|
233
|
+
// Repo-scoped guard for git:* requests. The PWA stamps `repoPath` on
|
|
234
|
+
// each request so a response can't be misapplied if the user has since
|
|
235
|
+
// switched repos. If the envelope's repoPath differs from the peer's
|
|
236
|
+
// current repo, reject with REPO_MISMATCH so the client can discard
|
|
237
|
+
// rather than render stale data.
|
|
238
|
+
if (message.type.startsWith('git:') &&
|
|
239
|
+
!message.type.endsWith(':response') &&
|
|
240
|
+
typeof message.repoPath === 'string') {
|
|
241
|
+
const currentRepo = this.getClientRepoPath(peerAddress);
|
|
242
|
+
if (message.repoPath !== currentRepo) {
|
|
243
|
+
const err = this.createErrorResponse(message.id, 'REPO_MISMATCH', `Repo mismatch: request expected ${message.repoPath}, peer is on ${currentRepo}`);
|
|
244
|
+
err.repoPath = currentRepo;
|
|
245
|
+
return err;
|
|
332
246
|
}
|
|
333
247
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
248
|
+
const response = await this.dispatch(message, peerAddress);
|
|
249
|
+
// Stamp git:* responses with the repo the agent actually used so the
|
|
250
|
+
// PWA can validate before applying to the store.
|
|
251
|
+
if (response.type.startsWith('git:') && response.type.endsWith(':response')) {
|
|
252
|
+
response.repoPath = this.getClientRepoPath(peerAddress);
|
|
253
|
+
}
|
|
254
|
+
return response;
|
|
255
|
+
}
|
|
256
|
+
async dispatch(message, peerAddress) {
|
|
257
|
+
switch (message.type) {
|
|
258
|
+
case 'handshake':
|
|
259
|
+
return this.handleHandshake(message, peerAddress);
|
|
260
|
+
case 'ping':
|
|
261
|
+
return createMessage('pong', { timestamp: Date.now() });
|
|
262
|
+
case 'git:status':
|
|
263
|
+
return this.handleStatus(message, peerAddress);
|
|
264
|
+
case 'git:diff':
|
|
265
|
+
return this.handleDiff(message, peerAddress);
|
|
266
|
+
case 'git:stage':
|
|
267
|
+
return this.handleStage(message, peerAddress);
|
|
268
|
+
case 'git:unstage':
|
|
269
|
+
return this.handleUnstage(message, peerAddress);
|
|
270
|
+
case 'git:stage-patch':
|
|
271
|
+
return this.handleStagePatch(message, peerAddress);
|
|
272
|
+
case 'git:unstage-patch':
|
|
273
|
+
return this.handleUnstagePatch(message, peerAddress);
|
|
274
|
+
case 'git:commit':
|
|
275
|
+
return this.handleCommit(message, peerAddress);
|
|
276
|
+
case 'git:log':
|
|
277
|
+
return this.handleLog(message, peerAddress);
|
|
278
|
+
case 'git:branches':
|
|
279
|
+
return this.handleBranches(peerAddress);
|
|
280
|
+
case 'git:checkout':
|
|
281
|
+
return this.handleCheckout(message, peerAddress);
|
|
282
|
+
case 'git:discard':
|
|
283
|
+
return this.handleDiscard(message, peerAddress);
|
|
284
|
+
case 'git:untrack':
|
|
285
|
+
return this.handleUntrack(message, peerAddress);
|
|
286
|
+
case 'git:submodules':
|
|
287
|
+
return this.handleSubmodules(message, peerAddress);
|
|
288
|
+
case 'git:config-get':
|
|
289
|
+
return this.handleGitConfigGet(message, peerAddress);
|
|
290
|
+
case 'git:config-set':
|
|
291
|
+
return this.handleGitConfigSet(message, peerAddress);
|
|
292
|
+
case 'git:gitignore-add':
|
|
293
|
+
return this.handleGitignoreAdd(message, peerAddress);
|
|
294
|
+
case 'git:gitignore-read':
|
|
295
|
+
return this.handleGitignoreRead(message, peerAddress);
|
|
296
|
+
case 'git:gitignore-write':
|
|
297
|
+
return this.handleGitignoreWrite(message, peerAddress);
|
|
298
|
+
case 'ai:generate-commit-summary':
|
|
299
|
+
return this.handleGenerateCommitSummary(message, peerAddress);
|
|
300
|
+
case 'ai:commit-summary:get':
|
|
301
|
+
return this.handleGetCommitSummary(message, peerAddress);
|
|
302
|
+
case 'ai:commit-summary:clear':
|
|
303
|
+
return this.handleClearCommitSummary(message, peerAddress);
|
|
304
|
+
case 'ai:set-api-key':
|
|
305
|
+
return this.handleSetApiKey(message);
|
|
306
|
+
case 'ai:get-api-key-status':
|
|
307
|
+
return this.handleGetApiKeyStatus(message);
|
|
308
|
+
case 'agent:list-repos':
|
|
309
|
+
return this.handleListRepos(message, peerAddress);
|
|
310
|
+
case 'agent:switch-repo':
|
|
311
|
+
return this.handleSwitchRepo(message, peerAddress);
|
|
312
|
+
case 'agent:browse-directory':
|
|
313
|
+
return this.handleBrowseDirectory(message);
|
|
314
|
+
case 'agent:add-repo':
|
|
315
|
+
return this.handleAddRepo(message);
|
|
316
|
+
case 'agent:remove-repo':
|
|
317
|
+
return this.handleRemoveRepo(message);
|
|
318
|
+
case 'agent:clone-repo':
|
|
319
|
+
return this.handleCloneRepo(message);
|
|
320
|
+
case 'agent:list-coding-paths':
|
|
321
|
+
return this.handleListCodingPaths(message);
|
|
322
|
+
case 'agent:add-coding-path':
|
|
323
|
+
return this.handleAddCodingPath(message);
|
|
324
|
+
case 'agent:remove-coding-path':
|
|
325
|
+
return this.handleRemoveCodingPath(message);
|
|
326
|
+
case 'agent:check-update':
|
|
327
|
+
return this.handleAgentCheckUpdate(message);
|
|
328
|
+
case 'agent:update':
|
|
329
|
+
return this.handleAgentUpdate(message);
|
|
330
|
+
case 'agent:restart':
|
|
331
|
+
return this.handleAgentRestart(message);
|
|
332
|
+
case 'codex:list-models':
|
|
333
|
+
return this.handleCodexListModels(message);
|
|
334
|
+
// Claude Code SDK
|
|
335
|
+
case 'claude:list-sessions':
|
|
336
|
+
return this.handleClaudeListSessions(message, peerAddress);
|
|
337
|
+
case 'claude:start':
|
|
338
|
+
return this.handleClaudeStart(message, peerAddress);
|
|
339
|
+
case 'claude:resume':
|
|
340
|
+
return this.handleClaudeResume(message, peerAddress);
|
|
341
|
+
case 'claude:cancel':
|
|
342
|
+
return this.handleClaudeCancel(message);
|
|
343
|
+
case 'claude:close':
|
|
344
|
+
return this.handleClaudeClose(message);
|
|
345
|
+
case 'claude:user-input-response':
|
|
346
|
+
return this.handleClaudeUserInputResponse(message);
|
|
347
|
+
case 'claude:get-preferences':
|
|
348
|
+
return this.handleGetPreferences(message);
|
|
349
|
+
case 'claude:set-preferences':
|
|
350
|
+
return this.handleSetPreferences(message);
|
|
351
|
+
case 'claude:set-session-permission':
|
|
352
|
+
return this.handleSetSessionPermission(message);
|
|
353
|
+
case 'claude:active-sessions':
|
|
354
|
+
return this.handleGetActiveSessions(message);
|
|
355
|
+
case 'claude:get-cards':
|
|
356
|
+
return this.handleClaudeGetCards(message, peerAddress);
|
|
357
|
+
case 'session:get-config':
|
|
358
|
+
return this.handleGetSessionConfig(message);
|
|
359
|
+
case 'session:set-config':
|
|
360
|
+
return this.handleSetSessionConfig(message);
|
|
361
|
+
case 'session:control-request':
|
|
362
|
+
return this.handleControlRequest(message);
|
|
363
|
+
case 'session:list-history':
|
|
364
|
+
return this.handleListHistory(message, peerAddress);
|
|
365
|
+
case 'session:update-history':
|
|
366
|
+
return this.handleUpdateHistory(message);
|
|
367
|
+
case 'session:delete-history':
|
|
368
|
+
return this.handleDeleteHistory(message);
|
|
369
|
+
case 'project:list-summaries':
|
|
370
|
+
return this.handleListProjectSummaries(message);
|
|
371
|
+
case 'project:list-repos':
|
|
372
|
+
return await this.handleListProjectRepos(message);
|
|
373
|
+
case 'push:subscription-offer':
|
|
374
|
+
return this.handlePushSubscriptionOffer(message);
|
|
375
|
+
default:
|
|
376
|
+
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
337
377
|
}
|
|
338
378
|
}
|
|
339
379
|
handleHandshake(message, peerAddress) {
|
|
@@ -492,6 +532,9 @@ export class MessageHandler {
|
|
|
492
532
|
try {
|
|
493
533
|
const { message: commitMessage, description, attribution } = message.payload;
|
|
494
534
|
const hash = await this.getGit(peerAddress).commit(commitMessage, description, attribution ?? true);
|
|
535
|
+
// The pending AI suggestion describes the diff we just committed, so
|
|
536
|
+
// it's stale now — clear it (also broadcasts state → idle to all peers).
|
|
537
|
+
this.commitSummaryStore.clear(repoPath);
|
|
495
538
|
const response = createMessage('git:commit:response', {
|
|
496
539
|
success: true,
|
|
497
540
|
hash,
|
|
@@ -711,70 +754,171 @@ export class MessageHandler {
|
|
|
711
754
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
712
755
|
}
|
|
713
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Kick off AI commit-summary generation. Generation runs asynchronously and
|
|
759
|
+
* streams progress + result via the `ai:commit-summary:updated` broadcast;
|
|
760
|
+
* the request-response just confirms the kickoff (or reports a validation
|
|
761
|
+
* failure that keeps the state idle, e.g. missing API key / no staged changes).
|
|
762
|
+
*/
|
|
714
763
|
async handleGenerateCommitSummary(message, peerAddress) {
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
764
|
+
const source = message.payload.source ?? 'api';
|
|
765
|
+
const repoPath = this.getClientRepoPath(peerAddress);
|
|
766
|
+
const respond = (payload) => {
|
|
767
|
+
const r = createMessage('ai:generate-commit-summary:response', payload);
|
|
768
|
+
r.id = message.id;
|
|
769
|
+
return r;
|
|
770
|
+
};
|
|
771
|
+
// API source requires an Anthropic key; CLI source does not.
|
|
772
|
+
let aiService = null;
|
|
773
|
+
if (source === 'api') {
|
|
774
|
+
aiService = this.getAiService();
|
|
775
|
+
if (!aiService) {
|
|
776
|
+
return respond({
|
|
777
|
+
success: false,
|
|
778
|
+
error: 'Configure your API key in Settings',
|
|
779
|
+
errorCode: 'NO_API_KEY',
|
|
780
|
+
});
|
|
781
|
+
}
|
|
724
782
|
}
|
|
783
|
+
// Validate staged changes exist before flipping the state into
|
|
784
|
+
// `generating` — we don't want to show "generating…" just to immediately
|
|
785
|
+
// error out on the same tick.
|
|
725
786
|
try {
|
|
726
|
-
const
|
|
727
|
-
const status = await git.getStatus();
|
|
787
|
+
const status = await this.getGit(peerAddress).getStatus();
|
|
728
788
|
if (status.staged.length === 0) {
|
|
729
|
-
|
|
789
|
+
return respond({
|
|
730
790
|
success: false,
|
|
731
791
|
error: 'No staged changes to summarize',
|
|
732
792
|
errorCode: 'NO_STAGED_CHANGES',
|
|
733
793
|
});
|
|
734
|
-
response.id = message.id;
|
|
735
|
-
return response;
|
|
736
794
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
return respond({
|
|
798
|
+
success: false,
|
|
799
|
+
error: error instanceof Error ? error.message : 'Failed to inspect working tree',
|
|
800
|
+
errorCode: 'API_ERROR',
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
// Register the generation with the state store. The returned token lets
|
|
804
|
+
// us detect supersession (the caller fired off a fresh generation while
|
|
805
|
+
// this one was still running, or the state was cleared).
|
|
806
|
+
let aborted = false;
|
|
807
|
+
let cliChild;
|
|
808
|
+
const token = this.commitSummaryStore.startGenerating(repoPath, source, message.payload.model, () => {
|
|
809
|
+
aborted = true;
|
|
810
|
+
try {
|
|
811
|
+
cliChild?.kill('SIGTERM');
|
|
812
|
+
}
|
|
813
|
+
catch { /* ignore */ }
|
|
814
|
+
});
|
|
815
|
+
// Run generation asynchronously. Errors flow through the state store,
|
|
816
|
+
// NOT through the kickoff response — the response has already been sent.
|
|
817
|
+
void this.runCommitSummary(peerAddress, repoPath, token, source, message.payload, aiService, (child) => {
|
|
818
|
+
cliChild = child;
|
|
819
|
+
if (aborted) {
|
|
820
|
+
try {
|
|
821
|
+
child.kill('SIGTERM');
|
|
822
|
+
}
|
|
823
|
+
catch { /* ignore */ }
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
return respond({ success: true, state: this.commitSummaryStore.get(repoPath) });
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Background worker that actually runs the generation. Reports success,
|
|
830
|
+
* progress, or failure through the CommitSummaryStateStore (which in turn
|
|
831
|
+
* broadcasts `ai:commit-summary:updated` to all peers).
|
|
832
|
+
*/
|
|
833
|
+
async runCommitSummary(peerAddress, repoPath, token, source, payload, aiService, onSpawn) {
|
|
834
|
+
try {
|
|
835
|
+
const git = this.getGit(peerAddress);
|
|
836
|
+
const [recentLog, branchInfo, conventions] = await Promise.all([
|
|
740
837
|
git.getLog(10).catch(() => []),
|
|
741
838
|
git.getBranches().catch(() => ({ current: '' })),
|
|
742
839
|
git.readCommitConventions().catch(() => undefined),
|
|
743
840
|
]);
|
|
744
841
|
const recentCommits = recentLog.map((c) => c.message);
|
|
745
842
|
const branchName = branchInfo.current;
|
|
746
|
-
|
|
843
|
+
if (source === 'claude-cli') {
|
|
844
|
+
const cliService = this.getAiCliService();
|
|
845
|
+
const result = await cliService.generateSummary({
|
|
846
|
+
repoPath,
|
|
847
|
+
context: payload.context,
|
|
848
|
+
model: payload.model,
|
|
849
|
+
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
850
|
+
branchName: branchName || undefined,
|
|
851
|
+
conventions,
|
|
852
|
+
attribution: payload.attribution,
|
|
853
|
+
onProgress: (progress) => {
|
|
854
|
+
this.commitSummaryStore.updateProgress(repoPath, token, progress);
|
|
855
|
+
},
|
|
856
|
+
onSpawn: (child) => {
|
|
857
|
+
onSpawn(child);
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
this.commitSummaryStore.setResult(repoPath, token, {
|
|
861
|
+
summary: result.summary,
|
|
862
|
+
description: result.description,
|
|
863
|
+
tokenUsage: result.tokenUsage,
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
// API path — fetch staged diffs fresh. No per-step progress today;
|
|
868
|
+
// the single Anthropic round-trip is fast enough that phase updates add
|
|
869
|
+
// little value. We still flip to `generating` so the UI shows a spinner.
|
|
870
|
+
this.commitSummaryStore.updateProgress(repoPath, token, { phase: 'generating' });
|
|
871
|
+
const status = await git.getStatus();
|
|
872
|
+
const diffs = await Promise.all(status.staged.map((file) => git.getDiff(file.path, true)));
|
|
747
873
|
const result = await aiService.generateSummary({
|
|
748
874
|
diffs,
|
|
749
|
-
context:
|
|
750
|
-
model:
|
|
875
|
+
context: payload.context,
|
|
876
|
+
model: payload.model,
|
|
751
877
|
recentCommits: recentCommits.length > 0 ? recentCommits : undefined,
|
|
752
878
|
branchName: branchName || undefined,
|
|
753
879
|
conventions,
|
|
754
|
-
attribution:
|
|
880
|
+
attribution: payload.attribution,
|
|
755
881
|
});
|
|
756
|
-
|
|
757
|
-
success: true,
|
|
882
|
+
this.commitSummaryStore.setResult(repoPath, token, {
|
|
758
883
|
summary: result.summary,
|
|
759
884
|
description: result.description,
|
|
760
885
|
tokenUsage: result.tokenUsage,
|
|
761
886
|
cached: result.cached,
|
|
762
887
|
});
|
|
763
|
-
response.id = message.id;
|
|
764
|
-
return response;
|
|
765
888
|
}
|
|
766
889
|
catch (error) {
|
|
890
|
+
if (error instanceof CommitSummaryCliError) {
|
|
891
|
+
this.commitSummaryStore.setError(repoPath, token, error.message, error.errorCode);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
767
894
|
const errorMessage = error instanceof Error ? error.message : 'Failed to generate summary';
|
|
768
895
|
const isRateLimit = errorMessage.includes('rate_limit');
|
|
769
|
-
|
|
770
|
-
success: false,
|
|
771
|
-
error: errorMessage,
|
|
772
|
-
errorCode: isRateLimit ? 'RATE_LIMITED' : 'API_ERROR',
|
|
773
|
-
});
|
|
774
|
-
response.id = message.id;
|
|
775
|
-
return response;
|
|
896
|
+
this.commitSummaryStore.setError(repoPath, token, errorMessage, isRateLimit ? 'RATE_LIMITED' : 'API_ERROR');
|
|
776
897
|
}
|
|
777
898
|
}
|
|
899
|
+
handleGetCommitSummary(message, peerAddress) {
|
|
900
|
+
const repoPath = message.payload.repoPath || this.getClientRepoPath(peerAddress);
|
|
901
|
+
const state = this.commitSummaryStore.get(repoPath);
|
|
902
|
+
const response = createMessage('ai:commit-summary:get:response', { state });
|
|
903
|
+
response.id = message.id;
|
|
904
|
+
return response;
|
|
905
|
+
}
|
|
906
|
+
handleClearCommitSummary(message, peerAddress) {
|
|
907
|
+
const repoPath = message.payload.repoPath || this.getClientRepoPath(peerAddress);
|
|
908
|
+
const state = this.commitSummaryStore.clear(repoPath);
|
|
909
|
+
const response = createMessage('ai:commit-summary:clear:response', {
|
|
910
|
+
success: true,
|
|
911
|
+
state,
|
|
912
|
+
});
|
|
913
|
+
response.id = message.id;
|
|
914
|
+
return response;
|
|
915
|
+
}
|
|
916
|
+
getAiCliService() {
|
|
917
|
+
if (!this.aiCliService) {
|
|
918
|
+
this.aiCliService = new CommitSummaryCliService();
|
|
919
|
+
}
|
|
920
|
+
return this.aiCliService;
|
|
921
|
+
}
|
|
778
922
|
handleSetApiKey(message) {
|
|
779
923
|
try {
|
|
780
924
|
setAnthropicApiKey(message.payload.apiKey);
|
|
@@ -1220,7 +1364,7 @@ export class MessageHandler {
|
|
|
1220
1364
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1221
1365
|
const streamId = generateMessageId();
|
|
1222
1366
|
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]' : ''}`);
|
|
1367
|
+
console.log(`[agent:start] agent=${resolvedAgent ?? 'default'} model=${model ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${sandboxed ? ' [sandboxed]' : ''}`);
|
|
1224
1368
|
try {
|
|
1225
1369
|
const sessionId = await this.claudeService.startSession({
|
|
1226
1370
|
prompt,
|
|
@@ -1234,9 +1378,15 @@ export class MessageHandler {
|
|
|
1234
1378
|
sandboxed,
|
|
1235
1379
|
});
|
|
1236
1380
|
console.log(`[agent:start] session created: ${sessionId} agent=${resolvedAgent ?? 'default'}`);
|
|
1237
|
-
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
1238
1381
|
// Register in session history
|
|
1239
1382
|
const now = Date.now();
|
|
1383
|
+
getEventStore().record({
|
|
1384
|
+
type: 'prompt_sent',
|
|
1385
|
+
sessionId,
|
|
1386
|
+
cwd,
|
|
1387
|
+
time: now,
|
|
1388
|
+
data: { kind: 'start', promptLength: prompt.length, model, agent: resolvedAgent },
|
|
1389
|
+
});
|
|
1240
1390
|
const registry = getSessionRegistry();
|
|
1241
1391
|
const gitBranch = await this.getGitBranchQuiet(cwd);
|
|
1242
1392
|
const actualAgent = this.claudeService.getSessionAgent(sessionId, cwd);
|
|
@@ -1269,11 +1419,8 @@ export class MessageHandler {
|
|
|
1269
1419
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1270
1420
|
const streamId = generateMessageId();
|
|
1271
1421
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
// immediately (user_message, then assistant turns), so we must be subscribed
|
|
1275
|
-
// before session.send() fires, not after resumeSession() returns.
|
|
1276
|
-
this.onPeerSubscribed?.(peerAddress, requestedId);
|
|
1422
|
+
const activeCfg = this.claudeService.getSessionConfig(requestedId);
|
|
1423
|
+
console.log(`[agent:resume] session=${requestedId} agent=${resolvedAgent ?? 'stored'} model=${activeCfg.model ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}`);
|
|
1277
1424
|
try {
|
|
1278
1425
|
const actualSessionId = await this.claudeService.resumeSession({
|
|
1279
1426
|
sessionId: requestedId,
|
|
@@ -1283,10 +1430,12 @@ export class MessageHandler {
|
|
|
1283
1430
|
agent: resolvedAgent,
|
|
1284
1431
|
});
|
|
1285
1432
|
console.log(`[agent:resume] session resumed: ${actualSessionId}`);
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1433
|
+
getEventStore().record({
|
|
1434
|
+
type: 'prompt_sent',
|
|
1435
|
+
sessionId: actualSessionId,
|
|
1436
|
+
cwd,
|
|
1437
|
+
data: { kind: 'resume', promptLength: prompt.length, agent: resolvedAgent },
|
|
1438
|
+
});
|
|
1290
1439
|
// Update session history
|
|
1291
1440
|
const registry = getSessionRegistry();
|
|
1292
1441
|
const existing = registry.getEntry(cwd, requestedId) ?? registry.getEntry(cwd, actualSessionId);
|
|
@@ -1314,6 +1463,9 @@ export class MessageHandler {
|
|
|
1314
1463
|
async handleClaudeCancel(message) {
|
|
1315
1464
|
const { sessionId } = message.payload;
|
|
1316
1465
|
const success = await this.claudeService.cancelSession(sessionId);
|
|
1466
|
+
if (success) {
|
|
1467
|
+
getEventStore().record({ type: 'session_cancelled', sessionId });
|
|
1468
|
+
}
|
|
1317
1469
|
const response = createMessage('claude:cancel:response', { success, error: success ? undefined : 'Session not found or already ended' });
|
|
1318
1470
|
response.id = message.id;
|
|
1319
1471
|
return response;
|
|
@@ -1339,7 +1491,6 @@ export class MessageHandler {
|
|
|
1339
1491
|
const cwd = payloadCwd || this.getClientRepoPath(peerAddress);
|
|
1340
1492
|
try {
|
|
1341
1493
|
const result = await this.claudeService.getCards(sessionId, cwd, offset, limit);
|
|
1342
|
-
this.onPeerSubscribed?.(peerAddress, sessionId);
|
|
1343
1494
|
const response = createMessage('claude:get-cards:response', result);
|
|
1344
1495
|
response.id = message.id;
|
|
1345
1496
|
return response;
|
|
@@ -1366,10 +1517,18 @@ export class MessageHandler {
|
|
|
1366
1517
|
response.id = message.id;
|
|
1367
1518
|
return response;
|
|
1368
1519
|
}
|
|
1369
|
-
handleSetSessionPermission(message) {
|
|
1520
|
+
async handleSetSessionPermission(message) {
|
|
1370
1521
|
const { sessionId, permissionMode } = message.payload;
|
|
1371
|
-
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
1372
|
-
|
|
1522
|
+
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'auto'];
|
|
1523
|
+
let success = false;
|
|
1524
|
+
if (validModes.includes(permissionMode)) {
|
|
1525
|
+
try {
|
|
1526
|
+
success = await this.claudeService.setPermissionLevel(sessionId, permissionMode);
|
|
1527
|
+
}
|
|
1528
|
+
catch {
|
|
1529
|
+
success = false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1373
1532
|
const response = createMessage('claude:set-session-permission:response', { success, sessionId, permissionMode });
|
|
1374
1533
|
response.id = message.id;
|
|
1375
1534
|
return response;
|
|
@@ -1381,12 +1540,43 @@ export class MessageHandler {
|
|
|
1381
1540
|
response.id = message.id;
|
|
1382
1541
|
return response;
|
|
1383
1542
|
}
|
|
1384
|
-
handleSetSessionConfig(message) {
|
|
1543
|
+
async handleSetSessionConfig(message) {
|
|
1385
1544
|
const { sessionId, key, value } = message.payload;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1545
|
+
console.log(`[agent:set-config] session=${sessionId.slice(0, 8)} ${key}=${String(value)}`);
|
|
1546
|
+
try {
|
|
1547
|
+
const config = await this.claudeService.setSessionConfig(sessionId, key, value);
|
|
1548
|
+
const response = createMessage('session:set-config:response', { success: true, sessionId, config });
|
|
1549
|
+
response.id = message.id;
|
|
1550
|
+
return response;
|
|
1551
|
+
}
|
|
1552
|
+
catch (err) {
|
|
1553
|
+
const config = this.claudeService.getSessionConfig(sessionId);
|
|
1554
|
+
const response = createMessage('session:set-config:response', {
|
|
1555
|
+
success: false,
|
|
1556
|
+
sessionId,
|
|
1557
|
+
config,
|
|
1558
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1559
|
+
});
|
|
1560
|
+
response.id = message.id;
|
|
1561
|
+
return response;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async handleControlRequest(message) {
|
|
1565
|
+
const { sessionId, subtype, params } = message.payload;
|
|
1566
|
+
console.log(`[agent:control-request] session=${sessionId.slice(0, 8)} subtype=${subtype} params=${JSON.stringify(params ?? {})}`);
|
|
1567
|
+
try {
|
|
1568
|
+
const result = await this.claudeService.sendControlRequest(sessionId, subtype, params);
|
|
1569
|
+
const response = createMessage('session:control-request:response', { success: true, sessionId, response: result });
|
|
1570
|
+
response.id = message.id;
|
|
1571
|
+
return response;
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1575
|
+
console.error(`[agent:control-request] error:`, errMsg);
|
|
1576
|
+
const response = createMessage('session:control-request:response', { success: false, sessionId, error: errMsg });
|
|
1577
|
+
response.id = message.id;
|
|
1578
|
+
return response;
|
|
1579
|
+
}
|
|
1390
1580
|
}
|
|
1391
1581
|
handleGetActiveSessions(message) {
|
|
1392
1582
|
const sessions = this.claudeService.getActiveSessions();
|
|
@@ -1456,6 +1646,20 @@ export class MessageHandler {
|
|
|
1456
1646
|
isGitRepo: existsSync(join(cwd, '.git')),
|
|
1457
1647
|
});
|
|
1458
1648
|
}
|
|
1649
|
+
// Include managed coding paths that have no sessions so the PWA can show them
|
|
1650
|
+
// in the project list. Without this, a freshly-added workspace with no sessions
|
|
1651
|
+
// would be pruned from the PWA's knownCodingPaths on next connect.
|
|
1652
|
+
for (const cwd of this.codingPaths.keys()) {
|
|
1653
|
+
if (byCwd.has(cwd))
|
|
1654
|
+
continue;
|
|
1655
|
+
projects.push({
|
|
1656
|
+
cwd,
|
|
1657
|
+
sessionCount: 0,
|
|
1658
|
+
lastActivityAt: 0,
|
|
1659
|
+
hasActiveSession: false,
|
|
1660
|
+
isGitRepo: existsSync(join(cwd, '.git')),
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1459
1663
|
// Sort by lastActivityAt desc
|
|
1460
1664
|
projects.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
1461
1665
|
const response = createMessage('project:list-summaries:response', { projects });
|
|
@@ -1558,6 +1762,21 @@ export class MessageHandler {
|
|
|
1558
1762
|
return undefined;
|
|
1559
1763
|
}
|
|
1560
1764
|
}
|
|
1765
|
+
async handlePushSubscriptionOffer(message) {
|
|
1766
|
+
const client = this.pushClient;
|
|
1767
|
+
let payload;
|
|
1768
|
+
if (!client) {
|
|
1769
|
+
payload = { success: false, error: 'push-not-configured' };
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
const { subscription, relayHttpUrl } = message.payload;
|
|
1773
|
+
const result = await client.register(subscription, relayHttpUrl);
|
|
1774
|
+
payload = result.ok ? { success: true } : { success: false, error: result.error ?? `http-${result.status}` };
|
|
1775
|
+
}
|
|
1776
|
+
const response = createMessage('push:subscription-offer:response', payload);
|
|
1777
|
+
response.id = message.id;
|
|
1778
|
+
return response;
|
|
1779
|
+
}
|
|
1561
1780
|
createErrorResponse(id, code, message) {
|
|
1562
1781
|
const response = createMessage('error', { code, message });
|
|
1563
1782
|
response.id = id;
|