@sumicom/quicksave 0.8.18 → 0.8.19
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 +12 -1
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +86 -8
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/codexAppServer/cardAdapter.d.ts.map +1 -1
- package/dist/ai/codexAppServer/cardAdapter.js +40 -4
- package/dist/ai/codexAppServer/cardAdapter.js.map +1 -1
- package/dist/handlers/legacyBusAdapter.d.ts +3 -3
- package/dist/handlers/legacyBusAdapter.d.ts.map +1 -1
- package/dist/handlers/legacyBusAdapter.js +9 -5
- package/dist/handlers/legacyBusAdapter.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +3 -3
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +150 -125
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/service/types.js +1 -1
- package/package.json +3 -3
|
@@ -93,11 +93,35 @@ function projectAppServerModel(m) {
|
|
|
93
93
|
isDefault: m.isDefault || undefined,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
function isRepoScopedRequest(message) {
|
|
97
|
+
if (message.type.startsWith('git:') && !message.type.endsWith(':response'))
|
|
98
|
+
return true;
|
|
99
|
+
return message.type === 'ai:generate-commit-summary' || message.type === 'ai:commit-summary:clear';
|
|
100
|
+
}
|
|
101
|
+
function payloadRepoPath(payload) {
|
|
102
|
+
if (typeof payload !== 'object' || payload === null)
|
|
103
|
+
return undefined;
|
|
104
|
+
const value = payload.repoPath;
|
|
105
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
106
|
+
}
|
|
107
|
+
async function hasPlausibleGitMarker(path) {
|
|
108
|
+
try {
|
|
109
|
+
const gitPath = join(path, '.git');
|
|
110
|
+
const marker = await stat(gitPath);
|
|
111
|
+
if (marker.isFile())
|
|
112
|
+
return true;
|
|
113
|
+
if (!marker.isDirectory())
|
|
114
|
+
return false;
|
|
115
|
+
return (await readdir(gitPath)).length > 0;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
96
121
|
export class MessageHandler {
|
|
97
122
|
repos;
|
|
98
123
|
agentVersion = '0.8.18';
|
|
99
124
|
defaultRepoPath;
|
|
100
|
-
clientRepos = new Map(); // peerAddress -> repoPath
|
|
101
125
|
repoLocks = new Map(); // repoPath -> peerAddress holding lock
|
|
102
126
|
availableRepos;
|
|
103
127
|
codingPaths = new Map(); // path -> CodingPath
|
|
@@ -364,24 +388,23 @@ export class MessageHandler {
|
|
|
364
388
|
if (this.defaultRepoPath === path) {
|
|
365
389
|
this.defaultRepoPath = this.availableRepos.length > 0 ? this.availableRepos[0].path : '';
|
|
366
390
|
}
|
|
367
|
-
// Drop stale per-peer pins to this path so the next handshake-ack
|
|
368
|
-
// doesn't return a deleted repo.
|
|
369
|
-
for (const [peer, repo] of this.clientRepos) {
|
|
370
|
-
if (repo === path)
|
|
371
|
-
this.clientRepos.delete(peer);
|
|
372
|
-
}
|
|
373
391
|
}
|
|
374
|
-
|
|
375
|
-
|
|
392
|
+
getRequiredRepoPath(message) {
|
|
393
|
+
if (!message.repoPath) {
|
|
394
|
+
throw new Error('Missing repoPath for repo-scoped request');
|
|
395
|
+
}
|
|
396
|
+
return message.repoPath;
|
|
376
397
|
}
|
|
377
|
-
|
|
378
|
-
const repoPath = this.getClientRepoPath(peerAddress);
|
|
398
|
+
getGitForRepoPath(repoPath) {
|
|
379
399
|
const git = this.repos.get(repoPath);
|
|
380
400
|
if (!git) {
|
|
381
|
-
throw new Error(
|
|
401
|
+
throw new Error(`Repository not available: ${repoPath}`);
|
|
382
402
|
}
|
|
383
403
|
return git;
|
|
384
404
|
}
|
|
405
|
+
getGit(message) {
|
|
406
|
+
return this.getGitForRepoPath(this.getRequiredRepoPath(message));
|
|
407
|
+
}
|
|
385
408
|
acquireRepoLock(repoPath, peerAddress) {
|
|
386
409
|
const holder = this.repoLocks.get(repoPath);
|
|
387
410
|
if (holder && holder !== peerAddress) {
|
|
@@ -396,14 +419,6 @@ export class MessageHandler {
|
|
|
396
419
|
}
|
|
397
420
|
}
|
|
398
421
|
removeClient(peerAddress) {
|
|
399
|
-
// Intentionally keep `clientRepos[peer]`: peer identity is a stable
|
|
400
|
-
// public key, and the PWA keeps its selected repo across reconnects
|
|
401
|
-
// (e.g., background → resume). Clearing would force the next
|
|
402
|
-
// handshake-ack to return `defaultRepoPath`, which the PWA would then
|
|
403
|
-
// hydrate into gitStore before its post-reconnect `agent:switch-repo`
|
|
404
|
-
// can land — racing `git:status` requests stamped with the default
|
|
405
|
-
// path would pass the REPO_MISMATCH guard and paint the default
|
|
406
|
-
// repo's (typically clean) status over the user's real repo view.
|
|
407
422
|
for (const [repoPath, holder] of this.repoLocks) {
|
|
408
423
|
if (holder === peerAddress) {
|
|
409
424
|
this.repoLocks.delete(repoPath);
|
|
@@ -464,39 +479,36 @@ export class MessageHandler {
|
|
|
464
479
|
if (isVerbose) {
|
|
465
480
|
console.log(`[msg] ${message.type} from ${peerAddress.slice(0, 12)}`);
|
|
466
481
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (message.repoPath !== currentRepo) {
|
|
477
|
-
const err = this.createErrorResponse(message.id, 'REPO_MISMATCH', `Repo mismatch: request expected ${message.repoPath}, peer is on ${currentRepo}`);
|
|
478
|
-
err.repoPath = currentRepo;
|
|
479
|
-
return err;
|
|
482
|
+
const repoScoped = isRepoScopedRequest(message);
|
|
483
|
+
if (repoScoped) {
|
|
484
|
+
const requestedRepoPath = message.repoPath ?? payloadRepoPath(message.payload);
|
|
485
|
+
if (!requestedRepoPath) {
|
|
486
|
+
return this.createErrorResponse(message.id, 'MISSING_REPO_PATH', `${message.type} requires repoPath`);
|
|
487
|
+
}
|
|
488
|
+
const repo = await this.ensureRepoAvailableForPath(requestedRepoPath);
|
|
489
|
+
if (!repo || !this.repos.has(repo.path)) {
|
|
490
|
+
return this.createErrorResponse(message.id, 'REPO_NOT_AVAILABLE', `Repository not available: ${requestedRepoPath}`);
|
|
480
491
|
}
|
|
492
|
+
message.repoPath = repo.path;
|
|
481
493
|
}
|
|
482
494
|
const response = await this.dispatch(message, peerAddress);
|
|
483
|
-
// Stamp
|
|
484
|
-
//
|
|
485
|
-
if (
|
|
486
|
-
response.repoPath =
|
|
495
|
+
// Stamp repo-scoped responses with the repo the agent actually used so
|
|
496
|
+
// clients can validate before applying the result to local UI state.
|
|
497
|
+
if (repoScoped && response.type !== 'error') {
|
|
498
|
+
response.repoPath = message.repoPath;
|
|
487
499
|
}
|
|
488
500
|
return response;
|
|
489
501
|
}
|
|
490
502
|
async dispatch(message, peerAddress) {
|
|
491
503
|
switch (message.type) {
|
|
492
504
|
case 'handshake':
|
|
493
|
-
return this.handleHandshake(message
|
|
505
|
+
return this.handleHandshake(message);
|
|
494
506
|
case 'ping':
|
|
495
507
|
return createMessage('pong', { timestamp: Date.now() });
|
|
496
508
|
case 'git:status':
|
|
497
|
-
return this.handleStatus(message
|
|
509
|
+
return this.handleStatus(message);
|
|
498
510
|
case 'git:diff':
|
|
499
|
-
return this.handleDiff(message
|
|
511
|
+
return this.handleDiff(message);
|
|
500
512
|
case 'git:stage':
|
|
501
513
|
return this.handleStage(message, peerAddress);
|
|
502
514
|
case 'git:unstage':
|
|
@@ -508,9 +520,9 @@ export class MessageHandler {
|
|
|
508
520
|
case 'git:commit':
|
|
509
521
|
return this.handleCommit(message, peerAddress);
|
|
510
522
|
case 'git:log':
|
|
511
|
-
return this.handleLog(message
|
|
523
|
+
return this.handleLog(message);
|
|
512
524
|
case 'git:branches':
|
|
513
|
-
return this.handleBranches(
|
|
525
|
+
return this.handleBranches(message);
|
|
514
526
|
case 'git:checkout':
|
|
515
527
|
return this.handleCheckout(message, peerAddress);
|
|
516
528
|
case 'git:discard':
|
|
@@ -518,29 +530,27 @@ export class MessageHandler {
|
|
|
518
530
|
case 'git:untrack':
|
|
519
531
|
return this.handleUntrack(message, peerAddress);
|
|
520
532
|
case 'git:submodules':
|
|
521
|
-
return this.handleSubmodules(message
|
|
533
|
+
return this.handleSubmodules(message);
|
|
522
534
|
case 'git:config-get':
|
|
523
|
-
return this.handleGitConfigGet(message
|
|
535
|
+
return this.handleGitConfigGet(message);
|
|
524
536
|
case 'git:config-set':
|
|
525
|
-
return this.handleGitConfigSet(message
|
|
537
|
+
return this.handleGitConfigSet(message);
|
|
526
538
|
case 'git:gitignore-add':
|
|
527
539
|
return this.handleGitignoreAdd(message, peerAddress);
|
|
528
540
|
case 'git:gitignore-read':
|
|
529
|
-
return this.handleGitignoreRead(message
|
|
541
|
+
return this.handleGitignoreRead(message);
|
|
530
542
|
case 'git:gitignore-write':
|
|
531
543
|
return this.handleGitignoreWrite(message, peerAddress);
|
|
532
544
|
case 'ai:generate-commit-summary':
|
|
533
|
-
return this.handleGenerateCommitSummary(message
|
|
545
|
+
return this.handleGenerateCommitSummary(message);
|
|
534
546
|
case 'ai:commit-summary:clear':
|
|
535
|
-
return this.handleClearCommitSummary(message
|
|
547
|
+
return this.handleClearCommitSummary(message);
|
|
536
548
|
case 'ai:set-api-key':
|
|
537
549
|
return this.handleSetApiKey(message);
|
|
538
550
|
case 'ai:get-api-key-status':
|
|
539
551
|
return this.handleGetApiKeyStatus(message);
|
|
540
552
|
case 'agent:list-repos':
|
|
541
|
-
return this.handleListRepos(message
|
|
542
|
-
case 'agent:switch-repo':
|
|
543
|
-
return this.handleSwitchRepo(message, peerAddress);
|
|
553
|
+
return this.handleListRepos(message);
|
|
544
554
|
case 'agent:browse-directory':
|
|
545
555
|
return this.handleBrowseDirectory(message);
|
|
546
556
|
case 'agent:add-repo':
|
|
@@ -601,7 +611,7 @@ export class MessageHandler {
|
|
|
601
611
|
case 'claude:set-session-permission':
|
|
602
612
|
return this.handleSetSessionPermission(message);
|
|
603
613
|
case 'claude:get-cards':
|
|
604
|
-
return this.handleClaudeGetCards(message
|
|
614
|
+
return this.handleClaudeGetCards(message);
|
|
605
615
|
case 'voice-agent:attach':
|
|
606
616
|
return this.handleVoiceAgentAttach(message);
|
|
607
617
|
case 'voice-agent:detach':
|
|
@@ -668,7 +678,7 @@ export class MessageHandler {
|
|
|
668
678
|
return this.createErrorResponse(message.id, 'UNKNOWN_MESSAGE_TYPE', `Unknown message type: ${message.type}`);
|
|
669
679
|
}
|
|
670
680
|
}
|
|
671
|
-
async handleHandshake(message
|
|
681
|
+
async handleHandshake(message) {
|
|
672
682
|
// Fire-and-forget: check npm registry + OpenAI models (12h dedup each)
|
|
673
683
|
this.checkLatestVersion().catch(() => { });
|
|
674
684
|
this.fetchCodexModels().catch(() => { });
|
|
@@ -676,7 +686,7 @@ export class MessageHandler {
|
|
|
676
686
|
const response = createMessage('handshake:ack', {
|
|
677
687
|
success: true,
|
|
678
688
|
agentVersion: this.agentVersion,
|
|
679
|
-
repoPath: this.
|
|
689
|
+
repoPath: this.defaultRepoPath,
|
|
680
690
|
availableRepos: this.availableRepos,
|
|
681
691
|
availableCodingPaths: [...this.codingPaths.values()],
|
|
682
692
|
preferences: this.claudeService.getPreferences(),
|
|
@@ -690,21 +700,21 @@ export class MessageHandler {
|
|
|
690
700
|
response.id = message.id;
|
|
691
701
|
return response;
|
|
692
702
|
}
|
|
693
|
-
async handleStatus(message
|
|
694
|
-
const status = await this.getGit(
|
|
703
|
+
async handleStatus(message) {
|
|
704
|
+
const status = await this.getGit(message).getStatus();
|
|
695
705
|
const response = createMessage('git:status:response', status);
|
|
696
706
|
response.id = message.id;
|
|
697
707
|
return response;
|
|
698
708
|
}
|
|
699
|
-
async handleDiff(message
|
|
709
|
+
async handleDiff(message) {
|
|
700
710
|
const { path, staged } = message.payload;
|
|
701
|
-
const diff = await this.getGit(
|
|
711
|
+
const diff = await this.getGit(message).getDiff(path, staged);
|
|
702
712
|
const response = createMessage('git:diff:response', diff);
|
|
703
713
|
response.id = message.id;
|
|
704
714
|
return response;
|
|
705
715
|
}
|
|
706
716
|
async handleStage(message, peerAddress) {
|
|
707
|
-
const repoPath = this.
|
|
717
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
708
718
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
709
719
|
const response = createMessage('git:stage:response', {
|
|
710
720
|
success: false,
|
|
@@ -714,7 +724,7 @@ export class MessageHandler {
|
|
|
714
724
|
return response;
|
|
715
725
|
}
|
|
716
726
|
try {
|
|
717
|
-
await this.getGit(
|
|
727
|
+
await this.getGit(message).stage(message.payload.paths);
|
|
718
728
|
const response = createMessage('git:stage:response', { success: true });
|
|
719
729
|
response.id = message.id;
|
|
720
730
|
return response;
|
|
@@ -732,7 +742,7 @@ export class MessageHandler {
|
|
|
732
742
|
}
|
|
733
743
|
}
|
|
734
744
|
async handleUnstage(message, peerAddress) {
|
|
735
|
-
const repoPath = this.
|
|
745
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
736
746
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
737
747
|
const response = createMessage('git:unstage:response', {
|
|
738
748
|
success: false,
|
|
@@ -742,7 +752,7 @@ export class MessageHandler {
|
|
|
742
752
|
return response;
|
|
743
753
|
}
|
|
744
754
|
try {
|
|
745
|
-
await this.getGit(
|
|
755
|
+
await this.getGit(message).unstage(message.payload.paths);
|
|
746
756
|
const response = createMessage('git:unstage:response', { success: true });
|
|
747
757
|
response.id = message.id;
|
|
748
758
|
return response;
|
|
@@ -760,7 +770,7 @@ export class MessageHandler {
|
|
|
760
770
|
}
|
|
761
771
|
}
|
|
762
772
|
async handleStagePatch(message, peerAddress) {
|
|
763
|
-
const repoPath = this.
|
|
773
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
764
774
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
765
775
|
const response = createMessage('git:stage-patch:response', {
|
|
766
776
|
success: false,
|
|
@@ -770,7 +780,7 @@ export class MessageHandler {
|
|
|
770
780
|
return response;
|
|
771
781
|
}
|
|
772
782
|
try {
|
|
773
|
-
await this.getGit(
|
|
783
|
+
await this.getGit(message).stagePatch(message.payload.patch);
|
|
774
784
|
const response = createMessage('git:stage-patch:response', { success: true });
|
|
775
785
|
response.id = message.id;
|
|
776
786
|
return response;
|
|
@@ -788,7 +798,7 @@ export class MessageHandler {
|
|
|
788
798
|
}
|
|
789
799
|
}
|
|
790
800
|
async handleUnstagePatch(message, peerAddress) {
|
|
791
|
-
const repoPath = this.
|
|
801
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
792
802
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
793
803
|
const response = createMessage('git:unstage-patch:response', {
|
|
794
804
|
success: false,
|
|
@@ -798,7 +808,7 @@ export class MessageHandler {
|
|
|
798
808
|
return response;
|
|
799
809
|
}
|
|
800
810
|
try {
|
|
801
|
-
await this.getGit(
|
|
811
|
+
await this.getGit(message).unstagePatch(message.payload.patch);
|
|
802
812
|
const response = createMessage('git:unstage-patch:response', { success: true });
|
|
803
813
|
response.id = message.id;
|
|
804
814
|
return response;
|
|
@@ -816,7 +826,7 @@ export class MessageHandler {
|
|
|
816
826
|
}
|
|
817
827
|
}
|
|
818
828
|
async handleCommit(message, peerAddress) {
|
|
819
|
-
const repoPath = this.
|
|
829
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
820
830
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
821
831
|
const response = createMessage('git:commit:response', {
|
|
822
832
|
success: false,
|
|
@@ -827,7 +837,7 @@ export class MessageHandler {
|
|
|
827
837
|
}
|
|
828
838
|
try {
|
|
829
839
|
const { message: commitMessage, description, attribution } = message.payload;
|
|
830
|
-
const hash = await this.getGit(
|
|
840
|
+
const hash = await this.getGit(message).commit(commitMessage, description, attribution ?? true);
|
|
831
841
|
// The pending AI suggestion describes the diff we just committed, so
|
|
832
842
|
// it's stale now — clear it (also broadcasts state → idle to all peers).
|
|
833
843
|
this.commitSummaryStore.clear(repoPath);
|
|
@@ -850,22 +860,22 @@ export class MessageHandler {
|
|
|
850
860
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
851
861
|
}
|
|
852
862
|
}
|
|
853
|
-
async handleLog(message
|
|
863
|
+
async handleLog(message) {
|
|
854
864
|
const limit = message.payload.limit || 50;
|
|
855
|
-
const commits = await this.getGit(
|
|
865
|
+
const commits = await this.getGit(message).getLog(limit);
|
|
856
866
|
const response = createMessage('git:log:response', { commits });
|
|
857
867
|
response.id = message.id;
|
|
858
868
|
return response;
|
|
859
869
|
}
|
|
860
|
-
async handleBranches(
|
|
861
|
-
const { branches, current } = await this.getGit(
|
|
870
|
+
async handleBranches(message) {
|
|
871
|
+
const { branches, current } = await this.getGit(message).getBranches();
|
|
862
872
|
return createMessage('git:branches:response', {
|
|
863
873
|
branches,
|
|
864
874
|
current,
|
|
865
875
|
});
|
|
866
876
|
}
|
|
867
877
|
async handleCheckout(message, peerAddress) {
|
|
868
|
-
const repoPath = this.
|
|
878
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
869
879
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
870
880
|
const response = createMessage('git:checkout:response', {
|
|
871
881
|
success: false,
|
|
@@ -876,7 +886,7 @@ export class MessageHandler {
|
|
|
876
886
|
}
|
|
877
887
|
try {
|
|
878
888
|
const { branch, create } = message.payload;
|
|
879
|
-
await this.getGit(
|
|
889
|
+
await this.getGit(message).checkout(branch, create);
|
|
880
890
|
const response = createMessage('git:checkout:response', { success: true });
|
|
881
891
|
response.id = message.id;
|
|
882
892
|
return response;
|
|
@@ -894,7 +904,7 @@ export class MessageHandler {
|
|
|
894
904
|
}
|
|
895
905
|
}
|
|
896
906
|
async handleDiscard(message, peerAddress) {
|
|
897
|
-
const repoPath = this.
|
|
907
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
898
908
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
899
909
|
const response = createMessage('git:discard:response', {
|
|
900
910
|
success: false,
|
|
@@ -904,7 +914,7 @@ export class MessageHandler {
|
|
|
904
914
|
return response;
|
|
905
915
|
}
|
|
906
916
|
try {
|
|
907
|
-
await this.getGit(
|
|
917
|
+
await this.getGit(message).discard(message.payload.paths);
|
|
908
918
|
const response = createMessage('git:discard:response', { success: true });
|
|
909
919
|
response.id = message.id;
|
|
910
920
|
return response;
|
|
@@ -922,7 +932,7 @@ export class MessageHandler {
|
|
|
922
932
|
}
|
|
923
933
|
}
|
|
924
934
|
async handleUntrack(message, peerAddress) {
|
|
925
|
-
const repoPath = this.
|
|
935
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
926
936
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
927
937
|
const response = createMessage('git:untrack:response', {
|
|
928
938
|
success: false,
|
|
@@ -932,7 +942,7 @@ export class MessageHandler {
|
|
|
932
942
|
return response;
|
|
933
943
|
}
|
|
934
944
|
try {
|
|
935
|
-
await this.getGit(
|
|
945
|
+
await this.getGit(message).untrack(message.payload.paths);
|
|
936
946
|
const response = createMessage('git:untrack:response', { success: true });
|
|
937
947
|
response.id = message.id;
|
|
938
948
|
return response;
|
|
@@ -949,10 +959,10 @@ export class MessageHandler {
|
|
|
949
959
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
950
960
|
}
|
|
951
961
|
}
|
|
952
|
-
async handleSubmodules(message
|
|
953
|
-
const git = this.getGit(
|
|
962
|
+
async handleSubmodules(message) {
|
|
963
|
+
const git = this.getGit(message);
|
|
954
964
|
const subs = await git.getSubmodules();
|
|
955
|
-
// Auto-register submodule paths so
|
|
965
|
+
// Auto-register submodule paths so later explicit repoPath requests work.
|
|
956
966
|
for (const sub of subs) {
|
|
957
967
|
if (!this.repos.has(sub.path)) {
|
|
958
968
|
this.repos.set(sub.path, new GitOperations(sub.path));
|
|
@@ -965,16 +975,16 @@ export class MessageHandler {
|
|
|
965
975
|
response.id = message.id;
|
|
966
976
|
return response;
|
|
967
977
|
}
|
|
968
|
-
async handleGitConfigGet(message
|
|
969
|
-
const identity = await this.getGit(
|
|
978
|
+
async handleGitConfigGet(message) {
|
|
979
|
+
const identity = await this.getGit(message).getIdentity();
|
|
970
980
|
const response = createMessage('git:config-get:response', identity);
|
|
971
981
|
response.id = message.id;
|
|
972
982
|
return response;
|
|
973
983
|
}
|
|
974
|
-
async handleGitConfigSet(message
|
|
984
|
+
async handleGitConfigSet(message) {
|
|
975
985
|
try {
|
|
976
986
|
const { name, email } = message.payload;
|
|
977
|
-
await this.getGit(
|
|
987
|
+
await this.getGit(message).setIdentity(name, email);
|
|
978
988
|
const response = createMessage('git:config-set:response', { success: true });
|
|
979
989
|
response.id = message.id;
|
|
980
990
|
return response;
|
|
@@ -989,7 +999,7 @@ export class MessageHandler {
|
|
|
989
999
|
}
|
|
990
1000
|
}
|
|
991
1001
|
async handleGitignoreAdd(message, peerAddress) {
|
|
992
|
-
const repoPath = this.
|
|
1002
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
993
1003
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
994
1004
|
const response = createMessage('git:gitignore-add:response', {
|
|
995
1005
|
success: false,
|
|
@@ -999,7 +1009,7 @@ export class MessageHandler {
|
|
|
999
1009
|
return response;
|
|
1000
1010
|
}
|
|
1001
1011
|
try {
|
|
1002
|
-
await this.getGit(
|
|
1012
|
+
await this.getGit(message).addToGitignore(message.payload.pattern);
|
|
1003
1013
|
const response = createMessage('git:gitignore-add:response', { success: true });
|
|
1004
1014
|
response.id = message.id;
|
|
1005
1015
|
return response;
|
|
@@ -1016,14 +1026,14 @@ export class MessageHandler {
|
|
|
1016
1026
|
this.releaseRepoLock(repoPath, peerAddress);
|
|
1017
1027
|
}
|
|
1018
1028
|
}
|
|
1019
|
-
async handleGitignoreRead(message
|
|
1020
|
-
const { content, exists } = await this.getGit(
|
|
1029
|
+
async handleGitignoreRead(message) {
|
|
1030
|
+
const { content, exists } = await this.getGit(message).readGitignore();
|
|
1021
1031
|
const response = createMessage('git:gitignore-read:response', { content, exists });
|
|
1022
1032
|
response.id = message.id;
|
|
1023
1033
|
return response;
|
|
1024
1034
|
}
|
|
1025
1035
|
async handleGitignoreWrite(message, peerAddress) {
|
|
1026
|
-
const repoPath = this.
|
|
1036
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
1027
1037
|
if (!this.acquireRepoLock(repoPath, peerAddress)) {
|
|
1028
1038
|
const response = createMessage('git:gitignore-write:response', {
|
|
1029
1039
|
success: false,
|
|
@@ -1033,7 +1043,7 @@ export class MessageHandler {
|
|
|
1033
1043
|
return response;
|
|
1034
1044
|
}
|
|
1035
1045
|
try {
|
|
1036
|
-
await this.getGit(
|
|
1046
|
+
await this.getGit(message).writeGitignore(message.payload.content);
|
|
1037
1047
|
const response = createMessage('git:gitignore-write:response', { success: true });
|
|
1038
1048
|
response.id = message.id;
|
|
1039
1049
|
return response;
|
|
@@ -1056,9 +1066,9 @@ export class MessageHandler {
|
|
|
1056
1066
|
* the request-response just confirms the kickoff (or reports a validation
|
|
1057
1067
|
* failure that keeps the state idle, e.g. missing API key / no staged changes).
|
|
1058
1068
|
*/
|
|
1059
|
-
async handleGenerateCommitSummary(message
|
|
1069
|
+
async handleGenerateCommitSummary(message) {
|
|
1060
1070
|
const source = message.payload.source ?? 'api';
|
|
1061
|
-
const repoPath = this.
|
|
1071
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
1062
1072
|
const respond = (payload) => {
|
|
1063
1073
|
const r = createMessage('ai:generate-commit-summary:response', payload);
|
|
1064
1074
|
r.id = message.id;
|
|
@@ -1080,7 +1090,7 @@ export class MessageHandler {
|
|
|
1080
1090
|
// `generating` — we don't want to show "generating…" just to immediately
|
|
1081
1091
|
// error out on the same tick.
|
|
1082
1092
|
try {
|
|
1083
|
-
const status = await this.getGit(
|
|
1093
|
+
const status = await this.getGit(message).getStatus();
|
|
1084
1094
|
if (status.staged.length === 0) {
|
|
1085
1095
|
return respond({
|
|
1086
1096
|
success: false,
|
|
@@ -1110,7 +1120,7 @@ export class MessageHandler {
|
|
|
1110
1120
|
});
|
|
1111
1121
|
// Run generation asynchronously. Errors flow through the state store,
|
|
1112
1122
|
// NOT through the kickoff response — the response has already been sent.
|
|
1113
|
-
void this.runCommitSummary(
|
|
1123
|
+
void this.runCommitSummary(repoPath, token, source, message.payload, aiService, (child) => {
|
|
1114
1124
|
cliChild = child;
|
|
1115
1125
|
if (aborted) {
|
|
1116
1126
|
try {
|
|
@@ -1126,9 +1136,9 @@ export class MessageHandler {
|
|
|
1126
1136
|
* progress, or failure through the CommitSummaryStateStore (which in turn
|
|
1127
1137
|
* broadcasts `ai:commit-summary:updated` to all peers).
|
|
1128
1138
|
*/
|
|
1129
|
-
async runCommitSummary(
|
|
1139
|
+
async runCommitSummary(repoPath, token, source, payload, aiService, onSpawn) {
|
|
1130
1140
|
try {
|
|
1131
|
-
const git = this.
|
|
1141
|
+
const git = this.getGitForRepoPath(repoPath);
|
|
1132
1142
|
const [recentLog, branchInfo, conventions] = await Promise.all([
|
|
1133
1143
|
git.getLog(10).catch(() => []),
|
|
1134
1144
|
git.getBranches().catch(() => ({ current: '' })),
|
|
@@ -1192,8 +1202,8 @@ export class MessageHandler {
|
|
|
1192
1202
|
this.commitSummaryStore.setError(repoPath, token, errorMessage, isRateLimit ? 'RATE_LIMITED' : 'API_ERROR');
|
|
1193
1203
|
}
|
|
1194
1204
|
}
|
|
1195
|
-
handleClearCommitSummary(message
|
|
1196
|
-
const repoPath =
|
|
1205
|
+
handleClearCommitSummary(message) {
|
|
1206
|
+
const repoPath = this.getRequiredRepoPath(message);
|
|
1197
1207
|
const state = this.commitSummaryStore.clear(repoPath);
|
|
1198
1208
|
const response = createMessage('ai:commit-summary:clear:response', {
|
|
1199
1209
|
success: true,
|
|
@@ -1290,7 +1300,7 @@ export class MessageHandler {
|
|
|
1290
1300
|
response.id = message.id;
|
|
1291
1301
|
return response;
|
|
1292
1302
|
}
|
|
1293
|
-
async handleListRepos(message
|
|
1303
|
+
async handleListRepos(message) {
|
|
1294
1304
|
// Refresh branch info for all repos
|
|
1295
1305
|
const repos = [];
|
|
1296
1306
|
for (const repo of this.availableRepos) {
|
|
@@ -1305,30 +1315,45 @@ export class MessageHandler {
|
|
|
1305
1315
|
}
|
|
1306
1316
|
const response = createMessage('agent:list-repos:response', {
|
|
1307
1317
|
repos,
|
|
1308
|
-
current: this.
|
|
1318
|
+
current: this.defaultRepoPath,
|
|
1309
1319
|
});
|
|
1310
1320
|
response.id = message.id;
|
|
1311
1321
|
return response;
|
|
1312
1322
|
}
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1323
|
+
async ensureRepoAvailableForPath(path) {
|
|
1324
|
+
const configured = this.availableRepos.find((repo) => repo.path === path);
|
|
1325
|
+
if (configured && this.repos.has(path))
|
|
1326
|
+
return configured;
|
|
1327
|
+
try {
|
|
1328
|
+
const requestedGit = new GitOperations(path);
|
|
1329
|
+
if (!(await requestedGit.isValidRepo()))
|
|
1330
|
+
return null;
|
|
1331
|
+
const rootPath = await requestedGit.getGitRoot();
|
|
1332
|
+
const existing = this.availableRepos.find((repo) => repo.path === rootPath);
|
|
1333
|
+
if (existing) {
|
|
1334
|
+
if (!this.repos.has(rootPath)) {
|
|
1335
|
+
this.repos.set(rootPath, new GitOperations(rootPath));
|
|
1336
|
+
}
|
|
1337
|
+
return existing;
|
|
1338
|
+
}
|
|
1339
|
+
const git = new GitOperations(rootPath);
|
|
1340
|
+
const { current } = await git.getBranches();
|
|
1341
|
+
const repo = {
|
|
1342
|
+
path: rootPath,
|
|
1343
|
+
name: basename(rootPath),
|
|
1344
|
+
currentBranch: current,
|
|
1345
|
+
};
|
|
1346
|
+
this.repos.set(rootPath, git);
|
|
1347
|
+
this.availableRepos.push(repo);
|
|
1348
|
+
if (!this.defaultRepoPath) {
|
|
1349
|
+
this.defaultRepoPath = rootPath;
|
|
1350
|
+
}
|
|
1351
|
+
addManagedRepo(rootPath);
|
|
1352
|
+
return repo;
|
|
1353
|
+
}
|
|
1354
|
+
catch {
|
|
1355
|
+
return null;
|
|
1324
1356
|
}
|
|
1325
|
-
this.clientRepos.set(peerAddress, path);
|
|
1326
|
-
const response = createMessage('agent:switch-repo:response', {
|
|
1327
|
-
success: true,
|
|
1328
|
-
newPath: path,
|
|
1329
|
-
});
|
|
1330
|
-
response.id = message.id;
|
|
1331
|
-
return response;
|
|
1332
1357
|
}
|
|
1333
1358
|
async handleBrowseDirectory(message) {
|
|
1334
1359
|
const requestedPath = message.payload.path || homedir();
|
|
@@ -1803,7 +1828,7 @@ export class MessageHandler {
|
|
|
1803
1828
|
async handleClaudeStart(message, peerAddress) {
|
|
1804
1829
|
const { prompt, cwd: payloadCwd, agent, allowedTools, systemPrompt, model, permissionMode, sandboxed, reasoningEffort, contextWindow, attachmentIds } = message.payload;
|
|
1805
1830
|
const legacyProvider = message.payload.provider;
|
|
1806
|
-
const cwd = payloadCwd || this.
|
|
1831
|
+
const cwd = payloadCwd || this.defaultRepoPath;
|
|
1807
1832
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1808
1833
|
// Coerce a Codex model the user's account doesn't actually support
|
|
1809
1834
|
// (e.g. they kept `claude-opus-4-7` selected before switching to a
|
|
@@ -1882,7 +1907,7 @@ export class MessageHandler {
|
|
|
1882
1907
|
async handleClaudeResume(message, peerAddress) {
|
|
1883
1908
|
const { sessionId: requestedId, prompt, cwd: payloadCwd, agent, attachmentIds, interruptCurrentTurn } = message.payload;
|
|
1884
1909
|
const legacyProvider = message.payload.provider;
|
|
1885
|
-
const cwd = payloadCwd || this.
|
|
1910
|
+
const cwd = payloadCwd || this.defaultRepoPath;
|
|
1886
1911
|
const resolvedAgent = agent ?? (legacyProvider === 'codex-mcp' ? 'codex' : legacyProvider ? 'claude-code' : undefined);
|
|
1887
1912
|
const activeCfg = this.claudeService.getSessionConfig(requestedId);
|
|
1888
1913
|
console.log(`[agent:resume] session=${requestedId} agent=${resolvedAgent ?? 'stored'} model=${activeCfg.model ?? 'default'} cwd=${cwd} prompt=${prompt.slice(0, 80)}${attachmentIds && attachmentIds.length > 0 ? ` [+${attachmentIds.length} attachments]` : ''}`);
|
|
@@ -2037,9 +2062,9 @@ export class MessageHandler {
|
|
|
2037
2062
|
response.id = message.id;
|
|
2038
2063
|
return response;
|
|
2039
2064
|
}
|
|
2040
|
-
async handleClaudeGetCards(message
|
|
2065
|
+
async handleClaudeGetCards(message) {
|
|
2041
2066
|
const { sessionId, cwd: payloadCwd, offset = 0, limit = 50 } = message.payload;
|
|
2042
|
-
const cwd = payloadCwd || this.
|
|
2067
|
+
const cwd = payloadCwd || this.defaultRepoPath;
|
|
2043
2068
|
try {
|
|
2044
2069
|
const result = await this.claudeService.getCards(sessionId, cwd, offset, limit);
|
|
2045
2070
|
const response = createMessage('claude:get-cards:response', result);
|
|
@@ -2462,7 +2487,7 @@ export class MessageHandler {
|
|
|
2462
2487
|
// detached HEAD returning empty `current`) doesn't drop the root
|
|
2463
2488
|
// and make the PWA's Git section flash empty. Branch + dirty are
|
|
2464
2489
|
// filled in best-effort below; undefined just means "unknown".
|
|
2465
|
-
const isRootRepo =
|
|
2490
|
+
const isRootRepo = await hasPlausibleGitMarker(cwd);
|
|
2466
2491
|
if (isRootRepo) {
|
|
2467
2492
|
repos.push({
|
|
2468
2493
|
path: cwd,
|
|
@@ -2521,7 +2546,7 @@ export class MessageHandler {
|
|
|
2521
2546
|
catch {
|
|
2522
2547
|
continue;
|
|
2523
2548
|
}
|
|
2524
|
-
if (
|
|
2549
|
+
if ((await hasPlausibleGitMarker(full)) && !seen.has(full)) {
|
|
2525
2550
|
seen.add(full);
|
|
2526
2551
|
const branch = await this.getGitBranchQuiet(full);
|
|
2527
2552
|
repos.push({
|