@ccpocket/bridge 1.30.0 → 1.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/git-assist.d.ts +7 -0
- package/dist/git-assist.js +51 -0
- package/dist/git-assist.js.map +1 -0
- package/dist/git-operations.d.ts +63 -0
- package/dist/git-operations.js +251 -0
- package/dist/git-operations.js.map +1 -0
- package/dist/parser.d.ts +125 -0
- package/dist/parser.js +188 -46
- package/dist/parser.js.map +1 -1
- package/dist/setup-launchd.d.ts +1 -0
- package/dist/setup-launchd.js +6 -0
- package/dist/setup-launchd.js.map +1 -1
- package/dist/setup-systemd.d.ts +1 -0
- package/dist/setup-systemd.js +4 -0
- package/dist/setup-systemd.js.map +1 -1
- package/dist/startup-info.d.ts +2 -1
- package/dist/startup-info.js +33 -9
- package/dist/startup-info.js.map +1 -1
- package/dist/websocket.js +991 -215
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -7,11 +7,13 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
7
7
|
import { SessionManager } from "./session.js";
|
|
8
8
|
import { SdkProcess } from "./sdk-process.js";
|
|
9
9
|
import { CodexProcess } from "./codex-process.js";
|
|
10
|
-
import { parseClientMessage } from "./parser.js";
|
|
11
|
-
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession } from "./sessions-index.js";
|
|
10
|
+
import { parseClientMessage, } from "./parser.js";
|
|
11
|
+
import { getAllRecentSessions, getCodexSessionHistory, getSessionHistory, findSessionsByClaudeIds, extractMessageImages, getClaudeSessionName, loadCodexSessionNames, renameClaudeSession, renameCodexSession, } from "./sessions-index.js";
|
|
12
12
|
import { ArchiveStore } from "./archive-store.js";
|
|
13
13
|
import { WorktreeStore } from "./worktree-store.js";
|
|
14
|
-
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch } from "./worktree.js";
|
|
14
|
+
import { listWorktrees, removeWorktree, worktreeExists, getMainBranch, } from "./worktree.js";
|
|
15
|
+
import { stageFiles, stageHunks, unstageFiles, unstageHunks, gitCommit, gitPush, listBranches, createBranch, checkoutBranch, revertFiles, revertHunks, gitFetch, gitPull, gitRemoteStatus, } from "./git-operations.js";
|
|
16
|
+
import { generateCommitMessage } from "./git-assist.js";
|
|
15
17
|
import { listWindows, takeScreenshot } from "./screenshot.js";
|
|
16
18
|
import { DebugTraceStore } from "./debug-trace-store.js";
|
|
17
19
|
import { PushRelayClient } from "./push-relay.js";
|
|
@@ -54,9 +56,8 @@ function deriveExecutionMode(params) {
|
|
|
54
56
|
return "default";
|
|
55
57
|
}
|
|
56
58
|
function derivePlanMode(params) {
|
|
57
|
-
return params.planMode ??
|
|
58
|
-
(
|
|
59
|
-
(params.collaborationMode === "plan"));
|
|
59
|
+
return (params.planMode ??
|
|
60
|
+
(params.permissionMode === "plan" || params.collaborationMode === "plan"));
|
|
60
61
|
}
|
|
61
62
|
function modesToLegacyPermissionMode(provider, executionMode, planMode) {
|
|
62
63
|
if (planMode)
|
|
@@ -138,7 +139,7 @@ export class BridgeWebSocketServer {
|
|
|
138
139
|
failSetPermissionMode = envFlagEnabled("BRIDGE_FAIL_SET_PERMISSION_MODE");
|
|
139
140
|
failSetSandboxMode = envFlagEnabled("BRIDGE_FAIL_SET_SANDBOX_MODE");
|
|
140
141
|
constructor(options) {
|
|
141
|
-
const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup } = options;
|
|
142
|
+
const { server, apiKey, allowedDirs, imageStore, galleryStore, projectHistory, debugTraceStore, recordingStore, firebaseAuth, promptHistoryBackup, } = options;
|
|
142
143
|
this.apiKey = apiKey ?? null;
|
|
143
144
|
this.allowedDirs = allowedDirs ?? [];
|
|
144
145
|
this.imageStore = imageStore ?? null;
|
|
@@ -223,30 +224,36 @@ export class BridgeWebSocketServer {
|
|
|
223
224
|
sessionId,
|
|
224
225
|
provider,
|
|
225
226
|
projectPath,
|
|
226
|
-
...(permissionMode
|
|
227
|
-
...((executionMode ?? (session?.process instanceof SdkProcess
|
|
228
|
-
? session.process.permissionMode === "bypassPermissions"
|
|
229
|
-
? "fullAccess"
|
|
230
|
-
: session.process.permissionMode === "acceptEdits"
|
|
231
|
-
? "acceptEdits"
|
|
232
|
-
: "default"
|
|
233
|
-
: session?.process instanceof CodexProcess
|
|
234
|
-
? session.process.approvalPolicy === "never"
|
|
235
|
-
? "fullAccess"
|
|
236
|
-
: "default"
|
|
237
|
-
: undefined))
|
|
227
|
+
...(permissionMode
|
|
238
228
|
? {
|
|
239
|
-
|
|
240
|
-
|
|
229
|
+
permissionMode: permissionMode,
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
...((executionMode ??
|
|
233
|
+
(session?.process instanceof SdkProcess
|
|
234
|
+
? session.process.permissionMode === "bypassPermissions"
|
|
235
|
+
? "fullAccess"
|
|
236
|
+
: session.process.permissionMode === "acceptEdits"
|
|
237
|
+
? "acceptEdits"
|
|
238
|
+
: "default"
|
|
239
|
+
: session?.process instanceof CodexProcess
|
|
240
|
+
? session.process.approvalPolicy === "never"
|
|
241
241
|
? "fullAccess"
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
: "default"
|
|
243
|
+
: undefined))
|
|
244
|
+
? {
|
|
245
|
+
executionMode: (executionMode ??
|
|
246
|
+
(session?.process instanceof SdkProcess
|
|
247
|
+
? session.process.permissionMode === "bypassPermissions"
|
|
247
248
|
? "fullAccess"
|
|
248
|
-
: "
|
|
249
|
-
|
|
249
|
+
: session.process.permissionMode === "acceptEdits"
|
|
250
|
+
? "acceptEdits"
|
|
251
|
+
: "default"
|
|
252
|
+
: session?.process instanceof CodexProcess
|
|
253
|
+
? session.process.approvalPolicy === "never"
|
|
254
|
+
? "fullAccess"
|
|
255
|
+
: "default"
|
|
256
|
+
: undefined)),
|
|
250
257
|
}
|
|
251
258
|
: {}),
|
|
252
259
|
...((planMode ??
|
|
@@ -254,8 +261,7 @@ export class BridgeWebSocketServer {
|
|
|
254
261
|
? session.process.permissionMode === "plan"
|
|
255
262
|
: session?.process instanceof CodexProcess
|
|
256
263
|
? session.process.collaborationMode === "plan"
|
|
257
|
-
: undefined)) !=
|
|
258
|
-
null
|
|
264
|
+
: undefined)) != null
|
|
259
265
|
? {
|
|
260
266
|
planMode: planMode ??
|
|
261
267
|
(session?.process instanceof SdkProcess
|
|
@@ -327,9 +333,12 @@ export class BridgeWebSocketServer {
|
|
|
327
333
|
// handle the unsupported message (suppress vs show update hint).
|
|
328
334
|
let rawType;
|
|
329
335
|
try {
|
|
330
|
-
rawType = JSON.parse(raw)
|
|
336
|
+
rawType = JSON.parse(raw)
|
|
337
|
+
?.type;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
/* ignore */
|
|
331
341
|
}
|
|
332
|
-
catch { /* ignore */ }
|
|
333
342
|
console.error("[ws] Unsupported message:", rawType ?? raw.slice(0, 200));
|
|
334
343
|
this.send(ws, {
|
|
335
344
|
type: "error",
|
|
@@ -350,7 +359,8 @@ export class BridgeWebSocketServer {
|
|
|
350
359
|
}
|
|
351
360
|
async handleClientMessage(msg, ws) {
|
|
352
361
|
const incomingSessionId = this.extractSessionIdFromClientMessage(msg);
|
|
353
|
-
const isActiveRuntimeSession = incomingSessionId != null &&
|
|
362
|
+
const isActiveRuntimeSession = incomingSessionId != null &&
|
|
363
|
+
this.sessionManager.get(incomingSessionId) != null;
|
|
354
364
|
if (incomingSessionId && isActiveRuntimeSession) {
|
|
355
365
|
this.recordDebugEvent(incomingSessionId, {
|
|
356
366
|
direction: "incoming",
|
|
@@ -381,7 +391,9 @@ export class BridgeWebSocketServer {
|
|
|
381
391
|
if (provider === "codex") {
|
|
382
392
|
console.log(`[ws] start(codex): execution=${executionMode} plan=${planMode}`);
|
|
383
393
|
}
|
|
384
|
-
const cached = provider === "claude"
|
|
394
|
+
const cached = provider === "claude"
|
|
395
|
+
? this.sessionManager.getCachedCommands(msg.projectPath)
|
|
396
|
+
: undefined;
|
|
385
397
|
const sessionId = this.sessionManager.create(msg.projectPath, {
|
|
386
398
|
sessionId: msg.sessionId,
|
|
387
399
|
continueMode: msg.continue,
|
|
@@ -408,9 +420,12 @@ export class BridgeWebSocketServer {
|
|
|
408
420
|
model: msg.model,
|
|
409
421
|
modelReasoningEffort: msg.modelReasoningEffort ?? undefined,
|
|
410
422
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
411
|
-
webSearchMode: msg.webSearchMode ??
|
|
423
|
+
webSearchMode: msg.webSearchMode ??
|
|
424
|
+
undefined,
|
|
412
425
|
threadId: msg.sessionId,
|
|
413
|
-
collaborationMode: planMode
|
|
426
|
+
collaborationMode: planMode
|
|
427
|
+
? "plan"
|
|
428
|
+
: "default",
|
|
414
429
|
}
|
|
415
430
|
: undefined);
|
|
416
431
|
const createdSession = this.sessionManager.get(sessionId);
|
|
@@ -464,20 +479,31 @@ export class BridgeWebSocketServer {
|
|
|
464
479
|
}
|
|
465
480
|
catch (err) {
|
|
466
481
|
console.error(`[ws] Failed to start session:`, err);
|
|
467
|
-
this.send(ws, {
|
|
482
|
+
this.send(ws, {
|
|
483
|
+
type: "error",
|
|
484
|
+
message: `Failed to start session: ${err.message}`,
|
|
485
|
+
});
|
|
468
486
|
}
|
|
469
487
|
break;
|
|
470
488
|
}
|
|
471
489
|
case "input": {
|
|
472
490
|
const session = this.resolveSession(msg.sessionId);
|
|
473
491
|
if (!session) {
|
|
474
|
-
this.send(ws, {
|
|
492
|
+
this.send(ws, {
|
|
493
|
+
type: "error",
|
|
494
|
+
message: "No active session. Send 'start' first.",
|
|
495
|
+
});
|
|
475
496
|
return;
|
|
476
497
|
}
|
|
477
498
|
const text = msg.text;
|
|
478
499
|
// Codex: reject if the process is not waiting for input (turn-based, no internal queue)
|
|
479
|
-
if (session.provider === "codex" &&
|
|
480
|
-
|
|
500
|
+
if (session.provider === "codex" &&
|
|
501
|
+
!session.process.isWaitingForInput) {
|
|
502
|
+
this.send(ws, {
|
|
503
|
+
type: "input_rejected",
|
|
504
|
+
sessionId: session.id,
|
|
505
|
+
reason: "Process is busy",
|
|
506
|
+
});
|
|
481
507
|
break;
|
|
482
508
|
}
|
|
483
509
|
// Snapshot busy state before dispatch. We prefer the actual enqueue
|
|
@@ -522,20 +548,28 @@ export class BridgeWebSocketServer {
|
|
|
522
548
|
// Persist images to Gallery Store asynchronously (fire-and-forget)
|
|
523
549
|
if (images.length > 0 && this.galleryStore && session.projectPath) {
|
|
524
550
|
for (const img of images) {
|
|
525
|
-
this.galleryStore
|
|
551
|
+
this.galleryStore
|
|
552
|
+
.addImageFromBase64(img.base64, img.mimeType, session.projectPath, msg.sessionId)
|
|
553
|
+
.catch((err) => {
|
|
526
554
|
console.warn(`[ws] Failed to persist image to gallery: ${err}`);
|
|
527
555
|
});
|
|
528
556
|
}
|
|
529
557
|
}
|
|
530
558
|
// Codex input path
|
|
531
559
|
if (session.provider === "codex") {
|
|
532
|
-
this.send(ws, {
|
|
560
|
+
this.send(ws, {
|
|
561
|
+
type: "input_ack",
|
|
562
|
+
sessionId: session.id,
|
|
563
|
+
queued: false,
|
|
564
|
+
});
|
|
533
565
|
const codexProc = session.process;
|
|
534
566
|
if (images.length > 0) {
|
|
535
567
|
codexProc.sendInputWithImages(text, images);
|
|
536
568
|
}
|
|
537
569
|
else if (msg.imageId && this.galleryStore) {
|
|
538
|
-
this.galleryStore
|
|
570
|
+
this.galleryStore
|
|
571
|
+
.getImageAsBase64(msg.imageId)
|
|
572
|
+
.then((imageData) => {
|
|
539
573
|
if (imageData) {
|
|
540
574
|
codexProc.sendInputWithImages(text, [imageData]);
|
|
541
575
|
}
|
|
@@ -543,7 +577,8 @@ export class BridgeWebSocketServer {
|
|
|
543
577
|
console.warn(`[ws] Image not found: ${msg.imageId}`);
|
|
544
578
|
codexProc.sendInput(text);
|
|
545
579
|
}
|
|
546
|
-
})
|
|
580
|
+
})
|
|
581
|
+
.catch((err) => {
|
|
547
582
|
console.error(`[ws] Failed to load image: ${err}`);
|
|
548
583
|
codexProc.sendInput(text);
|
|
549
584
|
});
|
|
@@ -562,7 +597,8 @@ export class BridgeWebSocketServer {
|
|
|
562
597
|
if (images.length > 0) {
|
|
563
598
|
console.log(`[ws] Sending message with ${images.length} inline Base64 image(s)`);
|
|
564
599
|
const result = claudeProc.sendInputWithImages(text, images);
|
|
565
|
-
wasQueued =
|
|
600
|
+
wasQueued =
|
|
601
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
566
602
|
}
|
|
567
603
|
// Legacy imageId mode (backward compatibility)
|
|
568
604
|
else if (msg.imageId && this.galleryStore) {
|
|
@@ -571,22 +607,29 @@ export class BridgeWebSocketServer {
|
|
|
571
607
|
sessionId: session.id,
|
|
572
608
|
queued: isAgentBusySnapshot,
|
|
573
609
|
});
|
|
574
|
-
this.galleryStore
|
|
610
|
+
this.galleryStore
|
|
611
|
+
.getImageAsBase64(msg.imageId)
|
|
612
|
+
.then((imageData) => {
|
|
575
613
|
let queuedAfterResolve = false;
|
|
576
614
|
if (imageData) {
|
|
577
|
-
const result = claudeProc.sendInputWithImages(text, [
|
|
578
|
-
|
|
615
|
+
const result = claudeProc.sendInputWithImages(text, [
|
|
616
|
+
imageData,
|
|
617
|
+
]);
|
|
618
|
+
queuedAfterResolve =
|
|
619
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
579
620
|
}
|
|
580
621
|
else {
|
|
581
622
|
console.warn(`[ws] Image not found: ${msg.imageId}`);
|
|
582
623
|
const result = session.process.sendInput(text);
|
|
583
|
-
queuedAfterResolve =
|
|
624
|
+
queuedAfterResolve =
|
|
625
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
584
626
|
}
|
|
585
627
|
if (queuedAfterResolve) {
|
|
586
628
|
console.log(`[ws] Agent is busy — will queue input and interrupt current turn`);
|
|
587
629
|
claudeProc.interrupt();
|
|
588
630
|
}
|
|
589
|
-
})
|
|
631
|
+
})
|
|
632
|
+
.catch((err) => {
|
|
590
633
|
console.error(`[ws] Failed to load image: ${err}`);
|
|
591
634
|
const result = session.process.sendInput(text);
|
|
592
635
|
const queuedAfterResolve = typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
@@ -600,12 +643,17 @@ export class BridgeWebSocketServer {
|
|
|
600
643
|
// Text-only message
|
|
601
644
|
else {
|
|
602
645
|
const result = session.process.sendInput(text);
|
|
603
|
-
wasQueued =
|
|
646
|
+
wasQueued =
|
|
647
|
+
typeof result === "boolean" ? result : isAgentBusySnapshot;
|
|
604
648
|
}
|
|
605
649
|
// Acknowledge receipt so the client can mark the message state.
|
|
606
650
|
// queued=true means the input was enqueued instead of being consumed
|
|
607
651
|
// immediately by the SDK stream.
|
|
608
|
-
this.send(ws, {
|
|
652
|
+
this.send(ws, {
|
|
653
|
+
type: "input_ack",
|
|
654
|
+
sessionId: session.id,
|
|
655
|
+
queued: wasQueued,
|
|
656
|
+
});
|
|
609
657
|
if (wasQueued) {
|
|
610
658
|
console.log(`[ws] Agent is busy — will queue input and interrupt current turn`);
|
|
611
659
|
claudeProc.interrupt();
|
|
@@ -617,34 +665,52 @@ export class BridgeWebSocketServer {
|
|
|
617
665
|
const privacyMode = msg.privacyMode === true;
|
|
618
666
|
console.log(`[ws] push_register received (platform: ${msg.platform}, locale: ${locale}, privacy: ${privacyMode}, configured: ${this.pushRelay.isConfigured})`);
|
|
619
667
|
if (!this.pushRelay.isConfigured) {
|
|
620
|
-
this.send(ws, {
|
|
668
|
+
this.send(ws, {
|
|
669
|
+
type: "error",
|
|
670
|
+
message: "Push relay is not configured on bridge",
|
|
671
|
+
});
|
|
621
672
|
return;
|
|
622
673
|
}
|
|
623
674
|
this.tokenLocales.set(msg.token, locale);
|
|
624
675
|
this.tokenPrivacyMode.set(msg.token, privacyMode);
|
|
625
|
-
this.pushRelay
|
|
676
|
+
this.pushRelay
|
|
677
|
+
.registerToken(msg.token, msg.platform, locale)
|
|
678
|
+
.then(() => {
|
|
626
679
|
console.log("[ws] push_register: token registered successfully");
|
|
627
|
-
})
|
|
680
|
+
})
|
|
681
|
+
.catch((err) => {
|
|
628
682
|
const detail = err instanceof Error ? err.message : String(err);
|
|
629
683
|
console.error(`[ws] push_register failed: ${detail}`);
|
|
630
|
-
this.send(ws, {
|
|
684
|
+
this.send(ws, {
|
|
685
|
+
type: "error",
|
|
686
|
+
message: `Failed to register push token: ${detail}`,
|
|
687
|
+
});
|
|
631
688
|
});
|
|
632
689
|
break;
|
|
633
690
|
}
|
|
634
691
|
case "push_unregister": {
|
|
635
692
|
console.log("[ws] push_unregister received");
|
|
636
693
|
if (!this.pushRelay.isConfigured) {
|
|
637
|
-
this.send(ws, {
|
|
694
|
+
this.send(ws, {
|
|
695
|
+
type: "error",
|
|
696
|
+
message: "Push relay is not configured on bridge",
|
|
697
|
+
});
|
|
638
698
|
return;
|
|
639
699
|
}
|
|
640
700
|
this.tokenLocales.delete(msg.token);
|
|
641
701
|
this.tokenPrivacyMode.delete(msg.token);
|
|
642
|
-
this.pushRelay
|
|
702
|
+
this.pushRelay
|
|
703
|
+
.unregisterToken(msg.token)
|
|
704
|
+
.then(() => {
|
|
643
705
|
console.log("[ws] push_unregister: token unregistered successfully");
|
|
644
|
-
})
|
|
706
|
+
})
|
|
707
|
+
.catch((err) => {
|
|
645
708
|
const detail = err instanceof Error ? err.message : String(err);
|
|
646
709
|
console.error(`[ws] push_unregister failed: ${detail}`);
|
|
647
|
-
this.send(ws, {
|
|
710
|
+
this.send(ws, {
|
|
711
|
+
type: "error",
|
|
712
|
+
message: `Failed to unregister push token: ${detail}`,
|
|
713
|
+
});
|
|
648
714
|
});
|
|
649
715
|
break;
|
|
650
716
|
}
|
|
@@ -677,10 +743,15 @@ export class BridgeWebSocketServer {
|
|
|
677
743
|
});
|
|
678
744
|
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
679
745
|
const newApproval = executionMode === "fullAccess" ? "never" : "on-request";
|
|
680
|
-
const newCollaboration = planMode
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
746
|
+
const newCollaboration = planMode
|
|
747
|
+
? "plan"
|
|
748
|
+
: "default";
|
|
749
|
+
const currentApproval = session.process
|
|
750
|
+
.approvalPolicy;
|
|
751
|
+
const currentCollaboration = session.process
|
|
752
|
+
.collaborationMode;
|
|
753
|
+
if (newApproval === currentApproval &&
|
|
754
|
+
newCollaboration === currentCollaboration) {
|
|
684
755
|
break; // No change needed
|
|
685
756
|
}
|
|
686
757
|
const canApplyModeInPlace = session.status === "idle";
|
|
@@ -721,9 +792,12 @@ export class BridgeWebSocketServer {
|
|
|
721
792
|
const sessionName = session.name;
|
|
722
793
|
this.sessionManager.destroy(oldSessionId);
|
|
723
794
|
console.log(`[ws] Permission mode change: destroyed session ${oldSessionId}`);
|
|
724
|
-
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
795
|
+
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
796
|
+
(session.pastMessages && session.pastMessages.length > 0);
|
|
725
797
|
if (!threadId || !hasUserMessages) {
|
|
726
|
-
const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
|
|
798
|
+
const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
|
|
799
|
+
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
800
|
+
: undefined, "codex", {
|
|
727
801
|
approvalPolicy: newApproval,
|
|
728
802
|
sandboxMode: oldSettings.sandboxMode,
|
|
729
803
|
model: oldSettings.model,
|
|
@@ -758,16 +832,26 @@ export class BridgeWebSocketServer {
|
|
|
758
832
|
let worktreeOpts;
|
|
759
833
|
if (wtMapping) {
|
|
760
834
|
if (worktreeExists(wtMapping.worktreePath)) {
|
|
761
|
-
worktreeOpts = {
|
|
835
|
+
worktreeOpts = {
|
|
836
|
+
existingWorktreePath: wtMapping.worktreePath,
|
|
837
|
+
worktreeBranch: wtMapping.worktreeBranch,
|
|
838
|
+
};
|
|
762
839
|
}
|
|
763
840
|
else {
|
|
764
|
-
worktreeOpts = {
|
|
841
|
+
worktreeOpts = {
|
|
842
|
+
useWorktree: true,
|
|
843
|
+
worktreeBranch: wtMapping.worktreeBranch,
|
|
844
|
+
};
|
|
765
845
|
}
|
|
766
846
|
}
|
|
767
847
|
else if (worktreePath) {
|
|
768
|
-
worktreeOpts = {
|
|
848
|
+
worktreeOpts = {
|
|
849
|
+
existingWorktreePath: worktreePath,
|
|
850
|
+
worktreeBranch,
|
|
851
|
+
};
|
|
769
852
|
}
|
|
770
|
-
getCodexSessionHistory(threadId)
|
|
853
|
+
getCodexSessionHistory(threadId)
|
|
854
|
+
.then((pastMessages) => {
|
|
771
855
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
772
856
|
threadId,
|
|
773
857
|
approvalPolicy: newApproval,
|
|
@@ -806,12 +890,18 @@ export class BridgeWebSocketServer {
|
|
|
806
890
|
detail: `mode=${msg.mode} approval=${newApproval} collaboration=${newCollaboration} thread=${threadId} oldSession=${oldSessionId}`,
|
|
807
891
|
});
|
|
808
892
|
console.log(`[ws] Permission mode change: created new session ${newId} (thread=${threadId}, mode=${msg.mode})`);
|
|
809
|
-
})
|
|
810
|
-
|
|
893
|
+
})
|
|
894
|
+
.catch((err) => {
|
|
895
|
+
this.send(ws, {
|
|
896
|
+
type: "error",
|
|
897
|
+
message: `Failed to restart session for permission mode change: ${err}`,
|
|
898
|
+
});
|
|
811
899
|
});
|
|
812
900
|
break;
|
|
813
901
|
}
|
|
814
|
-
session.process
|
|
902
|
+
session.process
|
|
903
|
+
.setPermissionMode(msg.mode)
|
|
904
|
+
.catch((err) => {
|
|
815
905
|
this.send(ws, {
|
|
816
906
|
type: "error",
|
|
817
907
|
message: `Failed to set permission mode: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -834,7 +924,10 @@ export class BridgeWebSocketServer {
|
|
|
834
924
|
return;
|
|
835
925
|
}
|
|
836
926
|
if (msg.sandboxMode !== "on" && msg.sandboxMode !== "off") {
|
|
837
|
-
this.send(ws, {
|
|
927
|
+
this.send(ws, {
|
|
928
|
+
type: "error",
|
|
929
|
+
message: `Invalid sandbox mode: ${msg.sandboxMode}`,
|
|
930
|
+
});
|
|
838
931
|
return;
|
|
839
932
|
}
|
|
840
933
|
// ---- Claude sandbox toggle ----
|
|
@@ -859,7 +952,9 @@ export class BridgeWebSocketServer {
|
|
|
859
952
|
permissionMode,
|
|
860
953
|
model,
|
|
861
954
|
sandboxEnabled: newEnabled,
|
|
862
|
-
}, undefined, worktreePath
|
|
955
|
+
}, undefined, worktreePath
|
|
956
|
+
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
957
|
+
: undefined, "claude");
|
|
863
958
|
const newSession = this.sessionManager.get(newId);
|
|
864
959
|
if (newSession && sessionName)
|
|
865
960
|
newSession.name = sessionName;
|
|
@@ -901,7 +996,8 @@ export class BridgeWebSocketServer {
|
|
|
901
996
|
const worktreePath = session.worktreePath;
|
|
902
997
|
const worktreeBranch = session.worktreeBranch;
|
|
903
998
|
const sessionName = session.name;
|
|
904
|
-
const collaborationMode = session.process
|
|
999
|
+
const collaborationMode = session.process
|
|
1000
|
+
.collaborationMode;
|
|
905
1001
|
const executionMode = oldSettings.approvalPolicy === "never" ? "fullAccess" : "default";
|
|
906
1002
|
const planMode = collaborationMode === "plan";
|
|
907
1003
|
const legacyPermissionMode = modesToLegacyPermissionMode("codex", executionMode, planMode);
|
|
@@ -911,13 +1007,16 @@ export class BridgeWebSocketServer {
|
|
|
911
1007
|
// session.history always contains system events (init, status, etc.)
|
|
912
1008
|
// even before the first user turn, so we check for user_input/assistant
|
|
913
1009
|
// messages specifically.
|
|
914
|
-
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
1010
|
+
const hasUserMessages = session.history?.some((m) => m.type === "user_input" || m.type === "assistant") ||
|
|
1011
|
+
(session.pastMessages && session.pastMessages.length > 0);
|
|
915
1012
|
if (!threadId || !hasUserMessages) {
|
|
916
1013
|
// Session has no thread yet, or has a thread but no messages exchanged.
|
|
917
1014
|
// Create a fresh session with the new sandbox — no resume needed.
|
|
918
1015
|
// (A thread with no messages cannot be resumed — Codex returns
|
|
919
1016
|
// "no rollout found for thread id".)
|
|
920
|
-
const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
|
|
1017
|
+
const newId = this.sessionManager.create(projectPath, undefined, undefined, worktreePath
|
|
1018
|
+
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
1019
|
+
: undefined, "codex", {
|
|
921
1020
|
approvalPolicy: oldSettings.approvalPolicy,
|
|
922
1021
|
sandboxMode: newSandboxMode,
|
|
923
1022
|
model: oldSettings.model,
|
|
@@ -950,16 +1049,23 @@ export class BridgeWebSocketServer {
|
|
|
950
1049
|
let worktreeOpts;
|
|
951
1050
|
if (wtMapping) {
|
|
952
1051
|
if (worktreeExists(wtMapping.worktreePath)) {
|
|
953
|
-
worktreeOpts = {
|
|
1052
|
+
worktreeOpts = {
|
|
1053
|
+
existingWorktreePath: wtMapping.worktreePath,
|
|
1054
|
+
worktreeBranch: wtMapping.worktreeBranch,
|
|
1055
|
+
};
|
|
954
1056
|
}
|
|
955
1057
|
else {
|
|
956
|
-
worktreeOpts = {
|
|
1058
|
+
worktreeOpts = {
|
|
1059
|
+
useWorktree: true,
|
|
1060
|
+
worktreeBranch: wtMapping.worktreeBranch,
|
|
1061
|
+
};
|
|
957
1062
|
}
|
|
958
1063
|
}
|
|
959
1064
|
else if (worktreePath) {
|
|
960
1065
|
worktreeOpts = { existingWorktreePath: worktreePath, worktreeBranch };
|
|
961
1066
|
}
|
|
962
|
-
getCodexSessionHistory(threadId)
|
|
1067
|
+
getCodexSessionHistory(threadId)
|
|
1068
|
+
.then((pastMessages) => {
|
|
963
1069
|
const newId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
964
1070
|
threadId,
|
|
965
1071
|
approvalPolicy: oldSettings.approvalPolicy,
|
|
@@ -997,8 +1103,12 @@ export class BridgeWebSocketServer {
|
|
|
997
1103
|
detail: `sandbox=${newSandboxMode} thread=${threadId} oldSession=${oldSessionId}`,
|
|
998
1104
|
});
|
|
999
1105
|
console.log(`[ws] Sandbox mode change: created new session ${newId} (thread=${threadId}, sandbox=${newSandboxMode})`);
|
|
1000
|
-
})
|
|
1001
|
-
|
|
1106
|
+
})
|
|
1107
|
+
.catch((err) => {
|
|
1108
|
+
this.send(ws, {
|
|
1109
|
+
type: "error",
|
|
1110
|
+
message: `Failed to restart session for sandbox mode change: ${err}`,
|
|
1111
|
+
});
|
|
1002
1112
|
});
|
|
1003
1113
|
break;
|
|
1004
1114
|
}
|
|
@@ -1043,7 +1153,9 @@ export class BridgeWebSocketServer {
|
|
|
1043
1153
|
: {}),
|
|
1044
1154
|
permissionMode,
|
|
1045
1155
|
initialInput: planText || undefined,
|
|
1046
|
-
}, undefined, worktreePath
|
|
1156
|
+
}, undefined, worktreePath
|
|
1157
|
+
? { existingWorktreePath: worktreePath, worktreeBranch }
|
|
1158
|
+
: undefined);
|
|
1047
1159
|
console.log(`[ws] Clear context: created new session ${newId} (CLI session: ${claudeSessionId ?? "new"})`);
|
|
1048
1160
|
// Notify all clients. Broadcast is used so reconnecting clients also receive it.
|
|
1049
1161
|
const newSession = this.sessionManager.get(newId);
|
|
@@ -1126,7 +1238,10 @@ export class BridgeWebSocketServer {
|
|
|
1126
1238
|
this.sendSessionList(ws);
|
|
1127
1239
|
}
|
|
1128
1240
|
else {
|
|
1129
|
-
this.send(ws, {
|
|
1241
|
+
this.send(ws, {
|
|
1242
|
+
type: "error",
|
|
1243
|
+
message: `Session ${msg.sessionId} not found`,
|
|
1244
|
+
});
|
|
1130
1245
|
}
|
|
1131
1246
|
break;
|
|
1132
1247
|
}
|
|
@@ -1142,8 +1257,16 @@ export class BridgeWebSocketServer {
|
|
|
1142
1257
|
messages: session.pastMessages,
|
|
1143
1258
|
});
|
|
1144
1259
|
}
|
|
1145
|
-
this.send(ws, {
|
|
1146
|
-
|
|
1260
|
+
this.send(ws, {
|
|
1261
|
+
type: "history",
|
|
1262
|
+
messages: session.history,
|
|
1263
|
+
sessionId: msg.sessionId,
|
|
1264
|
+
});
|
|
1265
|
+
this.send(ws, {
|
|
1266
|
+
type: "status",
|
|
1267
|
+
status: session.status,
|
|
1268
|
+
sessionId: msg.sessionId,
|
|
1269
|
+
});
|
|
1147
1270
|
// Send cached slash commands so the client can restore them even when
|
|
1148
1271
|
// the original init/supported_commands message was evicted from the
|
|
1149
1272
|
// in-memory history (MAX_HISTORY_PER_SESSION overflow).
|
|
@@ -1155,12 +1278,17 @@ export class BridgeWebSocketServer {
|
|
|
1155
1278
|
sessionId: msg.sessionId,
|
|
1156
1279
|
slashCommands: cached.slashCommands,
|
|
1157
1280
|
skills: cached.skills,
|
|
1158
|
-
...(cached.skillMetadata
|
|
1281
|
+
...(cached.skillMetadata
|
|
1282
|
+
? { skillMetadata: cached.skillMetadata }
|
|
1283
|
+
: {}),
|
|
1159
1284
|
});
|
|
1160
1285
|
}
|
|
1161
1286
|
}
|
|
1162
1287
|
else {
|
|
1163
|
-
this.send(ws, {
|
|
1288
|
+
this.send(ws, {
|
|
1289
|
+
type: "error",
|
|
1290
|
+
message: `Session ${msg.sessionId} not found`,
|
|
1291
|
+
});
|
|
1164
1292
|
}
|
|
1165
1293
|
break;
|
|
1166
1294
|
}
|
|
@@ -1171,10 +1299,13 @@ export class BridgeWebSocketServer {
|
|
|
1171
1299
|
let branch = "";
|
|
1172
1300
|
try {
|
|
1173
1301
|
branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1174
|
-
cwd,
|
|
1302
|
+
cwd,
|
|
1303
|
+
encoding: "utf-8",
|
|
1175
1304
|
}).trim();
|
|
1176
1305
|
}
|
|
1177
|
-
catch {
|
|
1306
|
+
catch {
|
|
1307
|
+
/* not a git repo */
|
|
1308
|
+
}
|
|
1178
1309
|
// Update stored branch so future session_list responses are also current
|
|
1179
1310
|
session.gitBranch = branch;
|
|
1180
1311
|
this.send(ws, {
|
|
@@ -1184,14 +1315,20 @@ export class BridgeWebSocketServer {
|
|
|
1184
1315
|
});
|
|
1185
1316
|
}
|
|
1186
1317
|
else {
|
|
1187
|
-
this.send(ws, {
|
|
1318
|
+
this.send(ws, {
|
|
1319
|
+
type: "error",
|
|
1320
|
+
message: `Session ${msg.sessionId} not found`,
|
|
1321
|
+
});
|
|
1188
1322
|
}
|
|
1189
1323
|
break;
|
|
1190
1324
|
}
|
|
1191
1325
|
case "get_debug_bundle": {
|
|
1192
1326
|
const session = this.sessionManager.get(msg.sessionId);
|
|
1193
1327
|
if (!session) {
|
|
1194
|
-
this.send(ws, {
|
|
1328
|
+
this.send(ws, {
|
|
1329
|
+
type: "error",
|
|
1330
|
+
message: `Session ${msg.sessionId} not found`,
|
|
1331
|
+
});
|
|
1195
1332
|
return;
|
|
1196
1333
|
}
|
|
1197
1334
|
const emitBundle = (diff, diffError) => {
|
|
@@ -1239,30 +1376,46 @@ export class BridgeWebSocketServer {
|
|
|
1239
1376
|
break;
|
|
1240
1377
|
}
|
|
1241
1378
|
case "get_usage": {
|
|
1242
|
-
fetchAllUsage()
|
|
1379
|
+
fetchAllUsage()
|
|
1380
|
+
.then((providers) => {
|
|
1243
1381
|
this.send(ws, { type: "usage_result", providers });
|
|
1244
|
-
})
|
|
1245
|
-
|
|
1382
|
+
})
|
|
1383
|
+
.catch((err) => {
|
|
1384
|
+
this.send(ws, {
|
|
1385
|
+
type: "error",
|
|
1386
|
+
message: `Failed to fetch usage: ${err}`,
|
|
1387
|
+
});
|
|
1246
1388
|
});
|
|
1247
1389
|
break;
|
|
1248
1390
|
}
|
|
1249
1391
|
case "list_recent_sessions": {
|
|
1250
1392
|
const requestId = ++this.recentSessionsRequestId;
|
|
1251
|
-
this.listRecentSessions(msg)
|
|
1393
|
+
this.listRecentSessions(msg)
|
|
1394
|
+
.then(({ sessions, hasMore }) => {
|
|
1252
1395
|
// Drop stale responses when rapid filter switches cause out-of-order completion
|
|
1253
1396
|
if (requestId !== this.recentSessionsRequestId)
|
|
1254
1397
|
return;
|
|
1255
|
-
this.send(ws, {
|
|
1256
|
-
|
|
1398
|
+
this.send(ws, {
|
|
1399
|
+
type: "recent_sessions",
|
|
1400
|
+
sessions,
|
|
1401
|
+
hasMore,
|
|
1402
|
+
});
|
|
1403
|
+
})
|
|
1404
|
+
.catch((err) => {
|
|
1257
1405
|
if (requestId !== this.recentSessionsRequestId)
|
|
1258
1406
|
return;
|
|
1259
|
-
this.send(ws, {
|
|
1407
|
+
this.send(ws, {
|
|
1408
|
+
type: "error",
|
|
1409
|
+
message: `Failed to list recent sessions: ${err}`,
|
|
1410
|
+
});
|
|
1260
1411
|
});
|
|
1261
1412
|
break;
|
|
1262
1413
|
}
|
|
1263
1414
|
case "archive_session": {
|
|
1264
1415
|
const { sessionId, provider, projectPath } = msg;
|
|
1265
|
-
this.archiveStore
|
|
1416
|
+
this.archiveStore
|
|
1417
|
+
.archive(sessionId, provider, projectPath)
|
|
1418
|
+
.then(() => {
|
|
1266
1419
|
// For Codex sessions, also call thread/archive RPC (best-effort).
|
|
1267
1420
|
// Requires a running Codex app-server process; skip if none active.
|
|
1268
1421
|
if (provider === "codex") {
|
|
@@ -1271,7 +1424,9 @@ export class BridgeWebSocketServer {
|
|
|
1271
1424
|
if (codexSession) {
|
|
1272
1425
|
const session = this.sessionManager.get(codexSession.id);
|
|
1273
1426
|
if (session) {
|
|
1274
|
-
session.process
|
|
1427
|
+
session.process
|
|
1428
|
+
.archiveThread(sessionId)
|
|
1429
|
+
.catch((err) => {
|
|
1275
1430
|
console.warn(`[ws] Codex thread/archive failed (non-fatal): ${err}`);
|
|
1276
1431
|
});
|
|
1277
1432
|
}
|
|
@@ -1282,7 +1437,8 @@ export class BridgeWebSocketServer {
|
|
|
1282
1437
|
sessionId,
|
|
1283
1438
|
success: true,
|
|
1284
1439
|
});
|
|
1285
|
-
})
|
|
1440
|
+
})
|
|
1441
|
+
.catch((err) => {
|
|
1286
1442
|
this.send(ws, {
|
|
1287
1443
|
type: "archive_result",
|
|
1288
1444
|
sessionId,
|
|
@@ -1330,7 +1486,8 @@ export class BridgeWebSocketServer {
|
|
|
1330
1486
|
};
|
|
1331
1487
|
}
|
|
1332
1488
|
}
|
|
1333
|
-
getCodexSessionHistory(sessionRefId)
|
|
1489
|
+
getCodexSessionHistory(sessionRefId)
|
|
1490
|
+
.then((pastMessages) => {
|
|
1334
1491
|
const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
|
|
1335
1492
|
threadId: sessionRefId,
|
|
1336
1493
|
approvalPolicy: executionMode === "fullAccess" ? "never" : "on-request",
|
|
@@ -1338,8 +1495,11 @@ export class BridgeWebSocketServer {
|
|
|
1338
1495
|
model: msg.model,
|
|
1339
1496
|
modelReasoningEffort: msg.modelReasoningEffort ?? undefined,
|
|
1340
1497
|
networkAccessEnabled: msg.networkAccessEnabled,
|
|
1341
|
-
webSearchMode: msg.webSearchMode ??
|
|
1342
|
-
|
|
1498
|
+
webSearchMode: msg.webSearchMode ??
|
|
1499
|
+
undefined,
|
|
1500
|
+
collaborationMode: planMode
|
|
1501
|
+
? "plan"
|
|
1502
|
+
: "default",
|
|
1343
1503
|
});
|
|
1344
1504
|
const createdSession = this.sessionManager.get(sessionId);
|
|
1345
1505
|
void this.loadAndSetSessionName(createdSession, "codex", effectiveProjectPath, sessionRefId).then(() => {
|
|
@@ -1365,8 +1525,12 @@ export class BridgeWebSocketServer {
|
|
|
1365
1525
|
detail: `provider=codex thread=${sessionRefId}`,
|
|
1366
1526
|
});
|
|
1367
1527
|
this.projectHistory?.addProject(effectiveProjectPath);
|
|
1368
|
-
})
|
|
1369
|
-
|
|
1528
|
+
})
|
|
1529
|
+
.catch((err) => {
|
|
1530
|
+
this.send(ws, {
|
|
1531
|
+
type: "error",
|
|
1532
|
+
message: `Failed to load Codex session history: ${err}`,
|
|
1533
|
+
});
|
|
1370
1534
|
});
|
|
1371
1535
|
break;
|
|
1372
1536
|
}
|
|
@@ -1385,10 +1549,14 @@ export class BridgeWebSocketServer {
|
|
|
1385
1549
|
}
|
|
1386
1550
|
else {
|
|
1387
1551
|
// Worktree was deleted — recreate on the same branch
|
|
1388
|
-
worktreeOpts = {
|
|
1552
|
+
worktreeOpts = {
|
|
1553
|
+
useWorktree: true,
|
|
1554
|
+
worktreeBranch: wtMapping.worktreeBranch,
|
|
1555
|
+
};
|
|
1389
1556
|
}
|
|
1390
1557
|
}
|
|
1391
|
-
getSessionHistory(claudeSessionId)
|
|
1558
|
+
getSessionHistory(claudeSessionId)
|
|
1559
|
+
.then((pastMessages) => {
|
|
1392
1560
|
const sessionId = this.sessionManager.create(msg.projectPath, {
|
|
1393
1561
|
sessionId: claudeSessionId,
|
|
1394
1562
|
permissionMode: legacyPermissionMode,
|
|
@@ -1399,7 +1567,9 @@ export class BridgeWebSocketServer {
|
|
|
1399
1567
|
fallbackModel: msg.fallbackModel,
|
|
1400
1568
|
forkSession: msg.forkSession,
|
|
1401
1569
|
persistSession: msg.persistSession,
|
|
1402
|
-
...(msg.sandboxMode
|
|
1570
|
+
...(msg.sandboxMode
|
|
1571
|
+
? { sandboxEnabled: msg.sandboxMode === "on" }
|
|
1572
|
+
: {}),
|
|
1403
1573
|
}, pastMessages, worktreeOpts);
|
|
1404
1574
|
const createdSession = this.sessionManager.get(sessionId);
|
|
1405
1575
|
void this.loadAndSetSessionName(createdSession, "claude", msg.projectPath, claudeSessionId).then(() => {
|
|
@@ -1435,8 +1605,12 @@ export class BridgeWebSocketServer {
|
|
|
1435
1605
|
detail: `provider=claude session=${claudeSessionId}`,
|
|
1436
1606
|
});
|
|
1437
1607
|
this.projectHistory?.addProject(msg.projectPath);
|
|
1438
|
-
})
|
|
1439
|
-
|
|
1608
|
+
})
|
|
1609
|
+
.catch((err) => {
|
|
1610
|
+
this.send(ws, {
|
|
1611
|
+
type: "error",
|
|
1612
|
+
message: `Failed to load session history: ${err}`,
|
|
1613
|
+
});
|
|
1440
1614
|
});
|
|
1441
1615
|
break;
|
|
1442
1616
|
}
|
|
@@ -1454,7 +1628,8 @@ export class BridgeWebSocketServer {
|
|
|
1454
1628
|
break;
|
|
1455
1629
|
}
|
|
1456
1630
|
case "get_message_images": {
|
|
1457
|
-
void extractMessageImages(msg.claudeSessionId, msg.messageUuid)
|
|
1631
|
+
void extractMessageImages(msg.claudeSessionId, msg.messageUuid)
|
|
1632
|
+
.then((images) => {
|
|
1458
1633
|
const refs = [];
|
|
1459
1634
|
if (this.imageStore) {
|
|
1460
1635
|
for (const img of images) {
|
|
@@ -1463,10 +1638,19 @@ export class BridgeWebSocketServer {
|
|
|
1463
1638
|
refs.push(ref);
|
|
1464
1639
|
}
|
|
1465
1640
|
}
|
|
1466
|
-
this.send(ws, {
|
|
1467
|
-
|
|
1641
|
+
this.send(ws, {
|
|
1642
|
+
type: "message_images_result",
|
|
1643
|
+
messageUuid: msg.messageUuid,
|
|
1644
|
+
images: refs,
|
|
1645
|
+
});
|
|
1646
|
+
})
|
|
1647
|
+
.catch((err) => {
|
|
1468
1648
|
console.error("[ws] Failed to extract message images:", err);
|
|
1469
|
-
this.send(ws, {
|
|
1649
|
+
this.send(ws, {
|
|
1650
|
+
type: "message_images_result",
|
|
1651
|
+
messageUuid: msg.messageUuid,
|
|
1652
|
+
images: [],
|
|
1653
|
+
});
|
|
1470
1654
|
});
|
|
1471
1655
|
break;
|
|
1472
1656
|
}
|
|
@@ -1493,32 +1677,70 @@ export class BridgeWebSocketServer {
|
|
|
1493
1677
|
case "read_file": {
|
|
1494
1678
|
const absPath = resolve(msg.projectPath, msg.filePath);
|
|
1495
1679
|
if (!this.isPathAllowed(absPath)) {
|
|
1496
|
-
this.send(ws, {
|
|
1680
|
+
this.send(ws, {
|
|
1681
|
+
type: "file_content",
|
|
1682
|
+
filePath: msg.filePath,
|
|
1683
|
+
content: "",
|
|
1684
|
+
error: "Path not allowed",
|
|
1685
|
+
});
|
|
1497
1686
|
break;
|
|
1498
1687
|
}
|
|
1499
1688
|
void (async () => {
|
|
1500
1689
|
try {
|
|
1501
1690
|
if (!existsSync(absPath)) {
|
|
1502
|
-
this.send(ws, {
|
|
1691
|
+
this.send(ws, {
|
|
1692
|
+
type: "file_content",
|
|
1693
|
+
filePath: msg.filePath,
|
|
1694
|
+
content: "",
|
|
1695
|
+
error: "File not found",
|
|
1696
|
+
});
|
|
1503
1697
|
return;
|
|
1504
1698
|
}
|
|
1505
|
-
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
1699
|
+
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
1700
|
+
? msg.maxLines
|
|
1701
|
+
: 5000;
|
|
1506
1702
|
const raw = await readFile(absPath, "utf-8");
|
|
1507
1703
|
const ext = extname(absPath).replace(/^\./, "").toLowerCase();
|
|
1508
1704
|
const languageMap = {
|
|
1509
|
-
ts: "typescript",
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1705
|
+
ts: "typescript",
|
|
1706
|
+
tsx: "typescript",
|
|
1707
|
+
js: "javascript",
|
|
1708
|
+
jsx: "javascript",
|
|
1709
|
+
py: "python",
|
|
1710
|
+
rb: "ruby",
|
|
1711
|
+
rs: "rust",
|
|
1712
|
+
go: "go",
|
|
1713
|
+
java: "java",
|
|
1714
|
+
kt: "kotlin",
|
|
1715
|
+
swift: "swift",
|
|
1716
|
+
dart: "dart",
|
|
1717
|
+
c: "c",
|
|
1718
|
+
cpp: "cpp",
|
|
1719
|
+
h: "c",
|
|
1720
|
+
hpp: "cpp",
|
|
1721
|
+
cs: "csharp",
|
|
1722
|
+
sh: "bash",
|
|
1723
|
+
zsh: "bash",
|
|
1724
|
+
yml: "yaml",
|
|
1725
|
+
yaml: "yaml",
|
|
1726
|
+
json: "json",
|
|
1727
|
+
toml: "toml",
|
|
1728
|
+
md: "markdown",
|
|
1729
|
+
html: "html",
|
|
1730
|
+
css: "css",
|
|
1731
|
+
scss: "css",
|
|
1732
|
+
sql: "sql",
|
|
1733
|
+
xml: "xml",
|
|
1734
|
+
dockerfile: "dockerfile",
|
|
1735
|
+
makefile: "makefile",
|
|
1736
|
+
gradle: "groovy",
|
|
1517
1737
|
};
|
|
1518
1738
|
const language = languageMap[ext] ?? (ext || undefined);
|
|
1519
1739
|
const lines = raw.split("\n");
|
|
1520
1740
|
const truncated = lines.length > maxLines;
|
|
1521
|
-
const content = truncated
|
|
1741
|
+
const content = truncated
|
|
1742
|
+
? lines.slice(0, maxLines).join("\n")
|
|
1743
|
+
: raw;
|
|
1522
1744
|
this.send(ws, {
|
|
1523
1745
|
type: "file_content",
|
|
1524
1746
|
filePath: msg.filePath,
|
|
@@ -1529,7 +1751,12 @@ export class BridgeWebSocketServer {
|
|
|
1529
1751
|
});
|
|
1530
1752
|
}
|
|
1531
1753
|
catch (err) {
|
|
1532
|
-
this.send(ws, {
|
|
1754
|
+
this.send(ws, {
|
|
1755
|
+
type: "file_content",
|
|
1756
|
+
filePath: msg.filePath,
|
|
1757
|
+
content: "",
|
|
1758
|
+
error: `Failed to read file: ${err}`,
|
|
1759
|
+
});
|
|
1533
1760
|
}
|
|
1534
1761
|
})();
|
|
1535
1762
|
break;
|
|
@@ -1546,7 +1773,10 @@ export class BridgeWebSocketServer {
|
|
|
1546
1773
|
this.send(ws, { type: "file_list", files: [] });
|
|
1547
1774
|
}
|
|
1548
1775
|
else {
|
|
1549
|
-
this.send(ws, {
|
|
1776
|
+
this.send(ws, {
|
|
1777
|
+
type: "error",
|
|
1778
|
+
message: `Failed to list files: ${err.message}`,
|
|
1779
|
+
});
|
|
1550
1780
|
}
|
|
1551
1781
|
return;
|
|
1552
1782
|
}
|
|
@@ -1612,15 +1842,27 @@ export class BridgeWebSocketServer {
|
|
|
1612
1842
|
}
|
|
1613
1843
|
case "get_recording": {
|
|
1614
1844
|
if (!this.recordingStore) {
|
|
1615
|
-
this.send(ws, {
|
|
1845
|
+
this.send(ws, {
|
|
1846
|
+
type: "error",
|
|
1847
|
+
message: "Recording is not enabled on this server",
|
|
1848
|
+
});
|
|
1616
1849
|
break;
|
|
1617
1850
|
}
|
|
1618
|
-
void this.recordingStore
|
|
1851
|
+
void this.recordingStore
|
|
1852
|
+
.getRecordingContent(msg.sessionId)
|
|
1853
|
+
.then((content) => {
|
|
1619
1854
|
if (content !== null) {
|
|
1620
|
-
this.send(ws, {
|
|
1855
|
+
this.send(ws, {
|
|
1856
|
+
type: "recording_content",
|
|
1857
|
+
sessionId: msg.sessionId,
|
|
1858
|
+
content,
|
|
1859
|
+
});
|
|
1621
1860
|
}
|
|
1622
1861
|
else {
|
|
1623
|
-
this.send(ws, {
|
|
1862
|
+
this.send(ws, {
|
|
1863
|
+
type: "error",
|
|
1864
|
+
message: `Recording ${msg.sessionId} not found`,
|
|
1865
|
+
});
|
|
1624
1866
|
}
|
|
1625
1867
|
});
|
|
1626
1868
|
break;
|
|
@@ -1641,7 +1883,11 @@ export class BridgeWebSocketServer {
|
|
|
1641
1883
|
});
|
|
1642
1884
|
}
|
|
1643
1885
|
else {
|
|
1644
|
-
this.send(ws, {
|
|
1886
|
+
this.send(ws, {
|
|
1887
|
+
type: "diff_result",
|
|
1888
|
+
diff: "",
|
|
1889
|
+
error: `Failed to get diff: ${error}`,
|
|
1890
|
+
});
|
|
1645
1891
|
}
|
|
1646
1892
|
return;
|
|
1647
1893
|
}
|
|
@@ -1653,11 +1899,16 @@ export class BridgeWebSocketServer {
|
|
|
1653
1899
|
this.send(ws, { type: "diff_result", diff });
|
|
1654
1900
|
}
|
|
1655
1901
|
});
|
|
1656
|
-
}
|
|
1902
|
+
}, msg.staged === true
|
|
1903
|
+
? { staged: true }
|
|
1904
|
+
: msg.staged === false
|
|
1905
|
+
? { unstaged: true }
|
|
1906
|
+
: undefined);
|
|
1657
1907
|
break;
|
|
1658
1908
|
}
|
|
1659
1909
|
case "get_diff_image": {
|
|
1660
|
-
if (!this.isPathAllowed(msg.projectPath) ||
|
|
1910
|
+
if (!this.isPathAllowed(msg.projectPath) ||
|
|
1911
|
+
!this.isPathAllowed(resolve(msg.projectPath, msg.filePath))) {
|
|
1661
1912
|
this.send(ws, { type: "error", message: `Path not allowed` });
|
|
1662
1913
|
break;
|
|
1663
1914
|
}
|
|
@@ -1689,7 +1940,12 @@ export class BridgeWebSocketServer {
|
|
|
1689
1940
|
void (async () => {
|
|
1690
1941
|
try {
|
|
1691
1942
|
const result = await this.loadDiffImageAsync(msg.projectPath, msg.filePath, version);
|
|
1692
|
-
this.send(ws, {
|
|
1943
|
+
this.send(ws, {
|
|
1944
|
+
type: "diff_image_result",
|
|
1945
|
+
filePath: msg.filePath,
|
|
1946
|
+
version,
|
|
1947
|
+
...result,
|
|
1948
|
+
});
|
|
1693
1949
|
}
|
|
1694
1950
|
catch {
|
|
1695
1951
|
// WebSocket may have closed; ignore send errors.
|
|
@@ -1709,7 +1965,10 @@ export class BridgeWebSocketServer {
|
|
|
1709
1965
|
this.send(ws, { type: "worktree_list", worktrees, mainBranch });
|
|
1710
1966
|
}
|
|
1711
1967
|
catch (err) {
|
|
1712
|
-
this.send(ws, {
|
|
1968
|
+
this.send(ws, {
|
|
1969
|
+
type: "error",
|
|
1970
|
+
message: `Failed to list worktrees: ${err}`,
|
|
1971
|
+
});
|
|
1713
1972
|
}
|
|
1714
1973
|
break;
|
|
1715
1974
|
}
|
|
@@ -1721,20 +1980,334 @@ export class BridgeWebSocketServer {
|
|
|
1721
1980
|
try {
|
|
1722
1981
|
removeWorktree(msg.projectPath, msg.worktreePath);
|
|
1723
1982
|
this.worktreeStore.deleteByWorktreePath(msg.worktreePath);
|
|
1724
|
-
this.send(ws, {
|
|
1983
|
+
this.send(ws, {
|
|
1984
|
+
type: "worktree_removed",
|
|
1985
|
+
worktreePath: msg.worktreePath,
|
|
1986
|
+
});
|
|
1725
1987
|
}
|
|
1726
1988
|
catch (err) {
|
|
1727
|
-
this.send(ws, {
|
|
1989
|
+
this.send(ws, {
|
|
1990
|
+
type: "error",
|
|
1991
|
+
message: `Failed to remove worktree: ${err}`,
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
break;
|
|
1995
|
+
}
|
|
1996
|
+
// ---- Git Operations (Phase 1-3) ----
|
|
1997
|
+
case "git_stage": {
|
|
1998
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
1999
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
if (msg.files?.length)
|
|
2004
|
+
stageFiles(msg.projectPath, msg.files);
|
|
2005
|
+
if (msg.hunks?.length)
|
|
2006
|
+
stageHunks(msg.projectPath, msg.hunks);
|
|
2007
|
+
this.send(ws, { type: "git_stage_result", success: true });
|
|
2008
|
+
}
|
|
2009
|
+
catch (err) {
|
|
2010
|
+
this.send(ws, {
|
|
2011
|
+
type: "git_stage_result",
|
|
2012
|
+
success: false,
|
|
2013
|
+
error: String(err),
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
case "git_unstage": {
|
|
2019
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2020
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
unstageFiles(msg.projectPath, msg.files ?? []);
|
|
2025
|
+
this.send(ws, { type: "git_unstage_result", success: true });
|
|
2026
|
+
}
|
|
2027
|
+
catch (err) {
|
|
2028
|
+
this.send(ws, {
|
|
2029
|
+
type: "git_unstage_result",
|
|
2030
|
+
success: false,
|
|
2031
|
+
error: String(err),
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
case "git_unstage_hunks": {
|
|
2037
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2038
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
unstageHunks(msg.projectPath, msg.hunks);
|
|
2043
|
+
this.send(ws, { type: "git_unstage_hunks_result", success: true });
|
|
2044
|
+
}
|
|
2045
|
+
catch (err) {
|
|
2046
|
+
this.send(ws, {
|
|
2047
|
+
type: "git_unstage_hunks_result",
|
|
2048
|
+
success: false,
|
|
2049
|
+
error: String(err),
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
break;
|
|
2053
|
+
}
|
|
2054
|
+
case "git_commit": {
|
|
2055
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2056
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
const session = msg.sessionId
|
|
2060
|
+
? this.sessionManager.get(msg.sessionId)
|
|
2061
|
+
: undefined;
|
|
2062
|
+
try {
|
|
2063
|
+
const message = msg.autoGenerate === true
|
|
2064
|
+
? (() => {
|
|
2065
|
+
if (!msg.sessionId) {
|
|
2066
|
+
throw new Error("git_commit with autoGenerate=true requires sessionId");
|
|
2067
|
+
}
|
|
2068
|
+
if (!session) {
|
|
2069
|
+
throw new Error(`Session ${msg.sessionId} not found`);
|
|
2070
|
+
}
|
|
2071
|
+
const expectedPath = resolve(session.worktreePath ?? session.projectPath);
|
|
2072
|
+
const requestedPath = resolve(msg.projectPath);
|
|
2073
|
+
if (requestedPath !== expectedPath) {
|
|
2074
|
+
throw new Error("git_commit projectPath must match the active session cwd");
|
|
2075
|
+
}
|
|
2076
|
+
return generateCommitMessage({
|
|
2077
|
+
provider: session.provider,
|
|
2078
|
+
projectPath: msg.projectPath,
|
|
2079
|
+
model: session.provider === "claude"
|
|
2080
|
+
? session.process instanceof SdkProcess
|
|
2081
|
+
? session.process.model
|
|
2082
|
+
: undefined
|
|
2083
|
+
: session.codexSettings?.model,
|
|
2084
|
+
});
|
|
2085
|
+
})()
|
|
2086
|
+
: msg.message ?? "";
|
|
2087
|
+
const result = gitCommit(msg.projectPath, message);
|
|
2088
|
+
this.send(ws, {
|
|
2089
|
+
type: "git_commit_result",
|
|
2090
|
+
success: true,
|
|
2091
|
+
commitHash: result.hash,
|
|
2092
|
+
message: result.message,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
catch (err) {
|
|
2096
|
+
this.send(ws, {
|
|
2097
|
+
type: "git_commit_result",
|
|
2098
|
+
success: false,
|
|
2099
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
break;
|
|
2103
|
+
}
|
|
2104
|
+
case "git_push": {
|
|
2105
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2106
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
try {
|
|
2110
|
+
gitPush(msg.projectPath);
|
|
2111
|
+
this.send(ws, {
|
|
2112
|
+
type: "git_push_result",
|
|
2113
|
+
success: true,
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
catch (err) {
|
|
2117
|
+
this.send(ws, {
|
|
2118
|
+
type: "git_push_result",
|
|
2119
|
+
success: false,
|
|
2120
|
+
error: String(err),
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
break;
|
|
2124
|
+
}
|
|
2125
|
+
case "git_branches": {
|
|
2126
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2127
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2128
|
+
break;
|
|
2129
|
+
}
|
|
2130
|
+
try {
|
|
2131
|
+
const result = listBranches(msg.projectPath);
|
|
2132
|
+
this.send(ws, {
|
|
2133
|
+
type: "git_branches_result",
|
|
2134
|
+
current: result.current,
|
|
2135
|
+
branches: result.branches,
|
|
2136
|
+
checkedOutBranches: result.checkedOutBranches,
|
|
2137
|
+
remoteStatusByBranch: result.remoteStatusByBranch,
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
catch (err) {
|
|
2141
|
+
this.send(ws, {
|
|
2142
|
+
type: "git_branches_result",
|
|
2143
|
+
current: "",
|
|
2144
|
+
branches: [],
|
|
2145
|
+
remoteStatusByBranch: {},
|
|
2146
|
+
error: String(err),
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2151
|
+
case "git_create_branch": {
|
|
2152
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2153
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2154
|
+
break;
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
createBranch(msg.projectPath, msg.name, msg.checkout);
|
|
2158
|
+
this.send(ws, { type: "git_create_branch_result", success: true });
|
|
2159
|
+
}
|
|
2160
|
+
catch (err) {
|
|
2161
|
+
this.send(ws, {
|
|
2162
|
+
type: "git_create_branch_result",
|
|
2163
|
+
success: false,
|
|
2164
|
+
error: String(err),
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
case "git_checkout_branch": {
|
|
2170
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2171
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
checkoutBranch(msg.projectPath, msg.branch);
|
|
2176
|
+
this.send(ws, { type: "git_checkout_branch_result", success: true });
|
|
2177
|
+
}
|
|
2178
|
+
catch (err) {
|
|
2179
|
+
this.send(ws, {
|
|
2180
|
+
type: "git_checkout_branch_result",
|
|
2181
|
+
success: false,
|
|
2182
|
+
error: String(err),
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
break;
|
|
2186
|
+
}
|
|
2187
|
+
case "git_revert_file": {
|
|
2188
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2189
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
try {
|
|
2193
|
+
revertFiles(msg.projectPath, msg.files);
|
|
2194
|
+
this.send(ws, { type: "git_revert_file_result", success: true });
|
|
2195
|
+
}
|
|
2196
|
+
catch (err) {
|
|
2197
|
+
this.send(ws, {
|
|
2198
|
+
type: "git_revert_file_result",
|
|
2199
|
+
success: false,
|
|
2200
|
+
error: String(err),
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
case "git_revert_hunks": {
|
|
2206
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2207
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
try {
|
|
2211
|
+
revertHunks(msg.projectPath, msg.hunks);
|
|
2212
|
+
this.send(ws, { type: "git_revert_hunks_result", success: true });
|
|
2213
|
+
}
|
|
2214
|
+
catch (err) {
|
|
2215
|
+
this.send(ws, {
|
|
2216
|
+
type: "git_revert_hunks_result",
|
|
2217
|
+
success: false,
|
|
2218
|
+
error: String(err),
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
case "git_fetch": {
|
|
2224
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2225
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
try {
|
|
2229
|
+
gitFetch(msg.projectPath);
|
|
2230
|
+
this.send(ws, { type: "git_fetch_result", success: true });
|
|
2231
|
+
}
|
|
2232
|
+
catch (err) {
|
|
2233
|
+
this.send(ws, {
|
|
2234
|
+
type: "git_fetch_result",
|
|
2235
|
+
success: false,
|
|
2236
|
+
error: String(err),
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
break;
|
|
2240
|
+
}
|
|
2241
|
+
case "git_pull": {
|
|
2242
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2243
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
try {
|
|
2247
|
+
const result = gitPull(msg.projectPath);
|
|
2248
|
+
if (result.success) {
|
|
2249
|
+
this.send(ws, {
|
|
2250
|
+
type: "git_pull_result",
|
|
2251
|
+
success: true,
|
|
2252
|
+
message: result.message,
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
else {
|
|
2256
|
+
this.send(ws, {
|
|
2257
|
+
type: "git_pull_result",
|
|
2258
|
+
success: false,
|
|
2259
|
+
error: result.message,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
catch (err) {
|
|
2264
|
+
this.send(ws, {
|
|
2265
|
+
type: "git_pull_result",
|
|
2266
|
+
success: false,
|
|
2267
|
+
error: String(err),
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
case "git_remote_status": {
|
|
2273
|
+
if (!this.isPathAllowed(msg.projectPath)) {
|
|
2274
|
+
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
try {
|
|
2278
|
+
const result = gitRemoteStatus(msg.projectPath);
|
|
2279
|
+
this.send(ws, {
|
|
2280
|
+
type: "git_remote_status_result",
|
|
2281
|
+
ahead: result.ahead,
|
|
2282
|
+
behind: result.behind,
|
|
2283
|
+
branch: result.branch,
|
|
2284
|
+
hasUpstream: result.hasUpstream,
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
catch (err) {
|
|
2288
|
+
this.send(ws, {
|
|
2289
|
+
type: "git_remote_status_result",
|
|
2290
|
+
ahead: 0,
|
|
2291
|
+
behind: 0,
|
|
2292
|
+
branch: "",
|
|
2293
|
+
hasUpstream: false,
|
|
2294
|
+
});
|
|
1728
2295
|
}
|
|
1729
2296
|
break;
|
|
1730
2297
|
}
|
|
1731
2298
|
case "rewind_dry_run": {
|
|
1732
2299
|
const session = this.sessionManager.get(msg.sessionId);
|
|
1733
2300
|
if (!session) {
|
|
1734
|
-
this.send(ws, {
|
|
2301
|
+
this.send(ws, {
|
|
2302
|
+
type: "rewind_preview",
|
|
2303
|
+
canRewind: false,
|
|
2304
|
+
error: `Session ${msg.sessionId} not found`,
|
|
2305
|
+
});
|
|
1735
2306
|
return;
|
|
1736
2307
|
}
|
|
1737
|
-
this.sessionManager
|
|
2308
|
+
this.sessionManager
|
|
2309
|
+
.rewindFiles(msg.sessionId, msg.targetUuid, true)
|
|
2310
|
+
.then((result) => {
|
|
1738
2311
|
this.send(ws, {
|
|
1739
2312
|
type: "rewind_preview",
|
|
1740
2313
|
canRewind: result.canRewind,
|
|
@@ -1743,40 +2316,73 @@ export class BridgeWebSocketServer {
|
|
|
1743
2316
|
deletions: result.deletions,
|
|
1744
2317
|
error: result.error,
|
|
1745
2318
|
});
|
|
1746
|
-
})
|
|
1747
|
-
|
|
2319
|
+
})
|
|
2320
|
+
.catch((err) => {
|
|
2321
|
+
this.send(ws, {
|
|
2322
|
+
type: "rewind_preview",
|
|
2323
|
+
canRewind: false,
|
|
2324
|
+
error: `Dry run failed: ${err}`,
|
|
2325
|
+
});
|
|
1748
2326
|
});
|
|
1749
2327
|
break;
|
|
1750
2328
|
}
|
|
1751
2329
|
case "rewind": {
|
|
1752
2330
|
const session = this.sessionManager.get(msg.sessionId);
|
|
1753
2331
|
if (!session) {
|
|
1754
|
-
this.send(ws, {
|
|
2332
|
+
this.send(ws, {
|
|
2333
|
+
type: "rewind_result",
|
|
2334
|
+
success: false,
|
|
2335
|
+
mode: msg.mode,
|
|
2336
|
+
error: `Session ${msg.sessionId} not found`,
|
|
2337
|
+
});
|
|
1755
2338
|
return;
|
|
1756
2339
|
}
|
|
1757
2340
|
const handleError = (err) => {
|
|
1758
2341
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1759
|
-
this.send(ws, {
|
|
2342
|
+
this.send(ws, {
|
|
2343
|
+
type: "rewind_result",
|
|
2344
|
+
success: false,
|
|
2345
|
+
mode: msg.mode,
|
|
2346
|
+
error: errMsg,
|
|
2347
|
+
});
|
|
1760
2348
|
};
|
|
1761
2349
|
if (msg.mode === "code") {
|
|
1762
2350
|
// Code-only rewind: rewind files without restarting the conversation
|
|
1763
|
-
this.sessionManager
|
|
2351
|
+
this.sessionManager
|
|
2352
|
+
.rewindFiles(msg.sessionId, msg.targetUuid)
|
|
2353
|
+
.then((result) => {
|
|
1764
2354
|
if (result.canRewind) {
|
|
1765
|
-
this.send(ws, {
|
|
2355
|
+
this.send(ws, {
|
|
2356
|
+
type: "rewind_result",
|
|
2357
|
+
success: true,
|
|
2358
|
+
mode: "code",
|
|
2359
|
+
});
|
|
1766
2360
|
}
|
|
1767
2361
|
else {
|
|
1768
|
-
this.send(ws, {
|
|
2362
|
+
this.send(ws, {
|
|
2363
|
+
type: "rewind_result",
|
|
2364
|
+
success: false,
|
|
2365
|
+
mode: "code",
|
|
2366
|
+
error: result.error ?? "Cannot rewind files",
|
|
2367
|
+
});
|
|
1769
2368
|
}
|
|
1770
|
-
})
|
|
2369
|
+
})
|
|
2370
|
+
.catch(handleError);
|
|
1771
2371
|
}
|
|
1772
2372
|
else if (msg.mode === "conversation") {
|
|
1773
2373
|
// Conversation-only rewind: restart session at the target UUID
|
|
1774
2374
|
try {
|
|
1775
2375
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
1776
|
-
this.send(ws, {
|
|
2376
|
+
this.send(ws, {
|
|
2377
|
+
type: "rewind_result",
|
|
2378
|
+
success: true,
|
|
2379
|
+
mode: "conversation",
|
|
2380
|
+
});
|
|
1777
2381
|
// Notify the new session ID
|
|
1778
2382
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1779
|
-
const rewindPermMode = newSession?.process instanceof SdkProcess
|
|
2383
|
+
const rewindPermMode = newSession?.process instanceof SdkProcess
|
|
2384
|
+
? newSession.process.permissionMode
|
|
2385
|
+
: undefined;
|
|
1780
2386
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1781
2387
|
sessionId: newSessionId,
|
|
1782
2388
|
provider: newSession?.provider ?? "claude",
|
|
@@ -1794,16 +2400,29 @@ export class BridgeWebSocketServer {
|
|
|
1794
2400
|
}
|
|
1795
2401
|
else {
|
|
1796
2402
|
// Both: rewind files first, then rewind conversation
|
|
1797
|
-
this.sessionManager
|
|
2403
|
+
this.sessionManager
|
|
2404
|
+
.rewindFiles(msg.sessionId, msg.targetUuid)
|
|
2405
|
+
.then((result) => {
|
|
1798
2406
|
if (!result.canRewind) {
|
|
1799
|
-
this.send(ws, {
|
|
2407
|
+
this.send(ws, {
|
|
2408
|
+
type: "rewind_result",
|
|
2409
|
+
success: false,
|
|
2410
|
+
mode: "both",
|
|
2411
|
+
error: result.error ?? "Cannot rewind files",
|
|
2412
|
+
});
|
|
1800
2413
|
return;
|
|
1801
2414
|
}
|
|
1802
2415
|
try {
|
|
1803
2416
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
1804
|
-
this.send(ws, {
|
|
2417
|
+
this.send(ws, {
|
|
2418
|
+
type: "rewind_result",
|
|
2419
|
+
success: true,
|
|
2420
|
+
mode: "both",
|
|
2421
|
+
});
|
|
1805
2422
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1806
|
-
const rewindPermMode2 = newSession?.process instanceof SdkProcess
|
|
2423
|
+
const rewindPermMode2 = newSession?.process instanceof SdkProcess
|
|
2424
|
+
? newSession.process.permissionMode
|
|
2425
|
+
: undefined;
|
|
1807
2426
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1808
2427
|
sessionId: newSessionId,
|
|
1809
2428
|
provider: newSession?.provider ?? "claude",
|
|
@@ -1818,7 +2437,8 @@ export class BridgeWebSocketServer {
|
|
|
1818
2437
|
catch (err) {
|
|
1819
2438
|
handleError(err);
|
|
1820
2439
|
}
|
|
1821
|
-
})
|
|
2440
|
+
})
|
|
2441
|
+
.catch(handleError);
|
|
1822
2442
|
}
|
|
1823
2443
|
break;
|
|
1824
2444
|
}
|
|
@@ -1859,7 +2479,11 @@ export class BridgeWebSocketServer {
|
|
|
1859
2479
|
const meta = await this.galleryStore.addImage(result.filePath, msg.projectPath, msg.sessionId);
|
|
1860
2480
|
if (meta) {
|
|
1861
2481
|
const info = this.galleryStore.metaToInfo(meta);
|
|
1862
|
-
this.send(ws, {
|
|
2482
|
+
this.send(ws, {
|
|
2483
|
+
type: "screenshot_result",
|
|
2484
|
+
success: true,
|
|
2485
|
+
image: info,
|
|
2486
|
+
});
|
|
1863
2487
|
this.broadcast({ type: "gallery_new_image", image: info });
|
|
1864
2488
|
return;
|
|
1865
2489
|
}
|
|
@@ -1886,23 +2510,44 @@ export class BridgeWebSocketServer {
|
|
|
1886
2510
|
}
|
|
1887
2511
|
case "backup_prompt_history": {
|
|
1888
2512
|
if (!this.promptHistoryBackup) {
|
|
1889
|
-
this.send(ws, {
|
|
2513
|
+
this.send(ws, {
|
|
2514
|
+
type: "prompt_history_backup_result",
|
|
2515
|
+
success: false,
|
|
2516
|
+
error: "Backup store not available",
|
|
2517
|
+
});
|
|
1890
2518
|
break;
|
|
1891
2519
|
}
|
|
1892
2520
|
const buf = Buffer.from(msg.data, "base64");
|
|
1893
|
-
this.promptHistoryBackup
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
this.send(ws, {
|
|
2521
|
+
this.promptHistoryBackup
|
|
2522
|
+
.save(buf, msg.appVersion, msg.dbVersion)
|
|
2523
|
+
.then((meta) => {
|
|
2524
|
+
this.send(ws, {
|
|
2525
|
+
type: "prompt_history_backup_result",
|
|
2526
|
+
success: true,
|
|
2527
|
+
backedUpAt: meta.backedUpAt,
|
|
2528
|
+
});
|
|
2529
|
+
})
|
|
2530
|
+
.catch((err) => {
|
|
2531
|
+
this.send(ws, {
|
|
2532
|
+
type: "prompt_history_backup_result",
|
|
2533
|
+
success: false,
|
|
2534
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2535
|
+
});
|
|
1897
2536
|
});
|
|
1898
2537
|
break;
|
|
1899
2538
|
}
|
|
1900
2539
|
case "restore_prompt_history": {
|
|
1901
2540
|
if (!this.promptHistoryBackup) {
|
|
1902
|
-
this.send(ws, {
|
|
2541
|
+
this.send(ws, {
|
|
2542
|
+
type: "prompt_history_restore_result",
|
|
2543
|
+
success: false,
|
|
2544
|
+
error: "Backup store not available",
|
|
2545
|
+
});
|
|
1903
2546
|
break;
|
|
1904
2547
|
}
|
|
1905
|
-
this.promptHistoryBackup
|
|
2548
|
+
this.promptHistoryBackup
|
|
2549
|
+
.load()
|
|
2550
|
+
.then((result) => {
|
|
1906
2551
|
if (result) {
|
|
1907
2552
|
this.send(ws, {
|
|
1908
2553
|
type: "prompt_history_restore_result",
|
|
@@ -1914,10 +2559,19 @@ export class BridgeWebSocketServer {
|
|
|
1914
2559
|
});
|
|
1915
2560
|
}
|
|
1916
2561
|
else {
|
|
1917
|
-
this.send(ws, {
|
|
2562
|
+
this.send(ws, {
|
|
2563
|
+
type: "prompt_history_restore_result",
|
|
2564
|
+
success: false,
|
|
2565
|
+
error: "No backup found",
|
|
2566
|
+
});
|
|
1918
2567
|
}
|
|
1919
|
-
})
|
|
1920
|
-
|
|
2568
|
+
})
|
|
2569
|
+
.catch((err) => {
|
|
2570
|
+
this.send(ws, {
|
|
2571
|
+
type: "prompt_history_restore_result",
|
|
2572
|
+
success: false,
|
|
2573
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2574
|
+
});
|
|
1921
2575
|
});
|
|
1922
2576
|
break;
|
|
1923
2577
|
}
|
|
@@ -1926,15 +2580,28 @@ export class BridgeWebSocketServer {
|
|
|
1926
2580
|
this.send(ws, { type: "prompt_history_backup_info", exists: false });
|
|
1927
2581
|
break;
|
|
1928
2582
|
}
|
|
1929
|
-
this.promptHistoryBackup
|
|
2583
|
+
this.promptHistoryBackup
|
|
2584
|
+
.getMeta()
|
|
2585
|
+
.then((meta) => {
|
|
1930
2586
|
if (meta) {
|
|
1931
|
-
this.send(ws, {
|
|
2587
|
+
this.send(ws, {
|
|
2588
|
+
type: "prompt_history_backup_info",
|
|
2589
|
+
exists: true,
|
|
2590
|
+
...meta,
|
|
2591
|
+
});
|
|
1932
2592
|
}
|
|
1933
2593
|
else {
|
|
1934
|
-
this.send(ws, {
|
|
2594
|
+
this.send(ws, {
|
|
2595
|
+
type: "prompt_history_backup_info",
|
|
2596
|
+
exists: false,
|
|
2597
|
+
});
|
|
1935
2598
|
}
|
|
1936
|
-
})
|
|
1937
|
-
|
|
2599
|
+
})
|
|
2600
|
+
.catch(() => {
|
|
2601
|
+
this.send(ws, {
|
|
2602
|
+
type: "prompt_history_backup_info",
|
|
2603
|
+
exists: false,
|
|
2604
|
+
});
|
|
1938
2605
|
});
|
|
1939
2606
|
break;
|
|
1940
2607
|
}
|
|
@@ -1981,10 +2648,12 @@ export class BridgeWebSocketServer {
|
|
|
1981
2648
|
if (runningSession) {
|
|
1982
2649
|
this.sessionManager.renameSession(sessionId, name);
|
|
1983
2650
|
// Persist to provider storage
|
|
1984
|
-
if (runningSession.provider === "claude" &&
|
|
2651
|
+
if (runningSession.provider === "claude" &&
|
|
2652
|
+
runningSession.claudeSessionId) {
|
|
1985
2653
|
await renameClaudeSession(runningSession.worktreePath ?? runningSession.projectPath, runningSession.claudeSessionId, name);
|
|
1986
2654
|
}
|
|
1987
|
-
else if (runningSession.provider === "codex" &&
|
|
2655
|
+
else if (runningSession.provider === "codex" &&
|
|
2656
|
+
runningSession.process) {
|
|
1988
2657
|
try {
|
|
1989
2658
|
await runningSession.process.renameThread(name ?? "");
|
|
1990
2659
|
}
|
|
@@ -2028,13 +2697,27 @@ export class BridgeWebSocketServer {
|
|
|
2028
2697
|
sendSessionList(ws) {
|
|
2029
2698
|
this.pruneDebugEvents();
|
|
2030
2699
|
const sessions = this.sessionManager.list();
|
|
2031
|
-
this.send(ws, {
|
|
2700
|
+
this.send(ws, {
|
|
2701
|
+
type: "session_list",
|
|
2702
|
+
sessions,
|
|
2703
|
+
allowedDirs: this.allowedDirs,
|
|
2704
|
+
claudeModels: CLAUDE_MODELS,
|
|
2705
|
+
codexModels: CODEX_MODELS,
|
|
2706
|
+
bridgeVersion: getPackageVersion(),
|
|
2707
|
+
});
|
|
2032
2708
|
}
|
|
2033
2709
|
/** Broadcast session list to all connected clients. */
|
|
2034
2710
|
broadcastSessionList() {
|
|
2035
2711
|
this.pruneDebugEvents();
|
|
2036
2712
|
const sessions = this.sessionManager.list();
|
|
2037
|
-
this.broadcast({
|
|
2713
|
+
this.broadcast({
|
|
2714
|
+
type: "session_list",
|
|
2715
|
+
sessions,
|
|
2716
|
+
allowedDirs: this.allowedDirs,
|
|
2717
|
+
claudeModels: CLAUDE_MODELS,
|
|
2718
|
+
codexModels: CODEX_MODELS,
|
|
2719
|
+
bridgeVersion: getPackageVersion(),
|
|
2720
|
+
});
|
|
2038
2721
|
}
|
|
2039
2722
|
broadcastSessionMessage(sessionId, msg) {
|
|
2040
2723
|
this.maybeSendPushNotification(sessionId, msg);
|
|
@@ -2046,7 +2729,9 @@ export class BridgeWebSocketServer {
|
|
|
2046
2729
|
});
|
|
2047
2730
|
this.recordingStore?.record(sessionId, "outgoing", msg);
|
|
2048
2731
|
// Update recording meta with claudeSessionId when it becomes available
|
|
2049
|
-
if ((msg.type === "system" || msg.type === "result") &&
|
|
2732
|
+
if ((msg.type === "system" || msg.type === "result") &&
|
|
2733
|
+
"sessionId" in msg &&
|
|
2734
|
+
msg.sessionId) {
|
|
2050
2735
|
const session = this.sessionManager.get(sessionId);
|
|
2051
2736
|
if (session) {
|
|
2052
2737
|
this.recordingStore?.saveMeta(sessionId, {
|
|
@@ -2085,16 +2770,21 @@ export class BridgeWebSocketServer {
|
|
|
2085
2770
|
});
|
|
2086
2771
|
}
|
|
2087
2772
|
getActiveCodexProcess() {
|
|
2088
|
-
const summary = this.sessionManager
|
|
2773
|
+
const summary = this.sessionManager
|
|
2774
|
+
.list()
|
|
2775
|
+
.find((session) => session.provider === "codex");
|
|
2089
2776
|
if (!summary)
|
|
2090
2777
|
return null;
|
|
2091
2778
|
const session = this.sessionManager.get(summary.id);
|
|
2092
|
-
return session?.provider === "codex"
|
|
2779
|
+
return session?.provider === "codex"
|
|
2780
|
+
? session.process
|
|
2781
|
+
: null;
|
|
2093
2782
|
}
|
|
2094
2783
|
async listRecentCodexThreads(msg) {
|
|
2095
2784
|
const limit = msg.limit ?? 20;
|
|
2096
2785
|
const offset = msg.offset ?? 0;
|
|
2097
|
-
const process = this.getActiveCodexProcess() ??
|
|
2786
|
+
const process = this.getActiveCodexProcess() ??
|
|
2787
|
+
(await this.createStandaloneCodexProcess(msg.projectPath));
|
|
2098
2788
|
const isStandalone = process !== this.getActiveCodexProcess();
|
|
2099
2789
|
try {
|
|
2100
2790
|
const result = await process.listThreads({
|
|
@@ -2179,7 +2869,9 @@ export class BridgeWebSocketServer {
|
|
|
2179
2869
|
this.notifiedPermissionToolUses.set(sessionId, seen);
|
|
2180
2870
|
const isAskUserQuestion = msg.toolName === "AskUserQuestion";
|
|
2181
2871
|
const isExitPlanMode = msg.toolName === "ExitPlanMode";
|
|
2182
|
-
const eventType = isAskUserQuestion
|
|
2872
|
+
const eventType = isAskUserQuestion
|
|
2873
|
+
? "ask_user_question"
|
|
2874
|
+
: "approval_required";
|
|
2183
2875
|
// Extract question text for AskUserQuestion (standard mode only)
|
|
2184
2876
|
let questionText;
|
|
2185
2877
|
if (!privacy && isAskUserQuestion) {
|
|
@@ -2202,30 +2894,38 @@ export class BridgeWebSocketServer {
|
|
|
2202
2894
|
let body;
|
|
2203
2895
|
if (isExitPlanMode) {
|
|
2204
2896
|
const titleKey = "plan_ready_title";
|
|
2205
|
-
title = label
|
|
2897
|
+
title = label
|
|
2898
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2899
|
+
: t(locale, titleKey);
|
|
2206
2900
|
body = t(locale, "plan_ready_body");
|
|
2207
2901
|
}
|
|
2208
2902
|
else if (isAskUserQuestion) {
|
|
2209
2903
|
const titleKey = "ask_title";
|
|
2210
|
-
title = label
|
|
2904
|
+
title = label
|
|
2905
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2906
|
+
: t(locale, titleKey);
|
|
2211
2907
|
body = privacy
|
|
2212
2908
|
? t(locale, "ask_body_private")
|
|
2213
2909
|
: (questionText ?? t(locale, "ask_default_body"));
|
|
2214
2910
|
}
|
|
2215
2911
|
else {
|
|
2216
2912
|
const titleKey = "approval_title";
|
|
2217
|
-
title = label
|
|
2913
|
+
title = label
|
|
2914
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2915
|
+
: t(locale, titleKey);
|
|
2218
2916
|
body = privacy
|
|
2219
2917
|
? t(locale, "approval_body_private")
|
|
2220
2918
|
: t(locale, "approval_body", { toolName: msg.toolName });
|
|
2221
2919
|
}
|
|
2222
|
-
void this.pushRelay
|
|
2920
|
+
void this.pushRelay
|
|
2921
|
+
.notify({
|
|
2223
2922
|
eventType,
|
|
2224
2923
|
title,
|
|
2225
2924
|
body,
|
|
2226
2925
|
locale,
|
|
2227
2926
|
data,
|
|
2228
|
-
})
|
|
2927
|
+
})
|
|
2928
|
+
.catch((err) => {
|
|
2229
2929
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2230
2930
|
console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
|
|
2231
2931
|
});
|
|
@@ -2260,12 +2960,18 @@ export class BridgeWebSocketServer {
|
|
|
2260
2960
|
for (const locale of this.getRegisteredLocales()) {
|
|
2261
2961
|
let title;
|
|
2262
2962
|
if (privacy) {
|
|
2263
|
-
title = isSuccess
|
|
2963
|
+
title = isSuccess
|
|
2964
|
+
? t(locale, "task_completed")
|
|
2965
|
+
: t(locale, "error_occurred");
|
|
2264
2966
|
}
|
|
2265
2967
|
else {
|
|
2266
2968
|
title = label
|
|
2267
|
-
?
|
|
2268
|
-
|
|
2969
|
+
? isSuccess
|
|
2970
|
+
? `✅ ${label}`
|
|
2971
|
+
: `❌ ${label}`
|
|
2972
|
+
: isSuccess
|
|
2973
|
+
? t(locale, "task_completed")
|
|
2974
|
+
: t(locale, "error_occurred");
|
|
2269
2975
|
}
|
|
2270
2976
|
let body;
|
|
2271
2977
|
if (privacy) {
|
|
@@ -2280,15 +2986,19 @@ export class BridgeWebSocketServer {
|
|
|
2280
2986
|
: `${t(locale, "session_completed")}${stats}`;
|
|
2281
2987
|
}
|
|
2282
2988
|
else {
|
|
2283
|
-
body = msg.error
|
|
2989
|
+
body = msg.error
|
|
2990
|
+
? msg.error.slice(0, 120)
|
|
2991
|
+
: t(locale, "session_failed");
|
|
2284
2992
|
}
|
|
2285
|
-
void this.pushRelay
|
|
2993
|
+
void this.pushRelay
|
|
2994
|
+
.notify({
|
|
2286
2995
|
eventType,
|
|
2287
2996
|
title,
|
|
2288
2997
|
body,
|
|
2289
2998
|
locale,
|
|
2290
2999
|
data,
|
|
2291
|
-
})
|
|
3000
|
+
})
|
|
3001
|
+
.catch((err) => {
|
|
2292
3002
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2293
3003
|
console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
|
|
2294
3004
|
});
|
|
@@ -2320,34 +3030,89 @@ export class BridgeWebSocketServer {
|
|
|
2320
3030
|
broadcastGalleryNewImage(image) {
|
|
2321
3031
|
this.broadcast({ type: "gallery_new_image", image });
|
|
2322
3032
|
}
|
|
2323
|
-
collectGitDiff(cwd, callback) {
|
|
3033
|
+
collectGitDiff(cwd, callback, options) {
|
|
2324
3034
|
const execOpts = { cwd, maxBuffer: 10 * 1024 * 1024 };
|
|
2325
|
-
//
|
|
2326
|
-
|
|
3035
|
+
// Staged only: git diff --cached
|
|
3036
|
+
if (options?.staged) {
|
|
3037
|
+
execFile("git", ["diff", "--cached", "--no-color"], execOpts, (err, stdout) => {
|
|
3038
|
+
if (err) {
|
|
3039
|
+
callback({ diff: "", error: err.message });
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
callback({ diff: stdout });
|
|
3043
|
+
});
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
// Unstaged only: git diff (working tree vs index) — original behavior
|
|
3047
|
+
if (options?.unstaged) {
|
|
3048
|
+
// Collect untracked files so they appear in the diff.
|
|
3049
|
+
let untrackedFiles = [];
|
|
3050
|
+
try {
|
|
3051
|
+
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd })
|
|
3052
|
+
.toString()
|
|
3053
|
+
.trim();
|
|
3054
|
+
untrackedFiles = out ? out.split("\n") : [];
|
|
3055
|
+
}
|
|
3056
|
+
catch {
|
|
3057
|
+
// Ignore errors: non-git directories are handled by git diff callback.
|
|
3058
|
+
}
|
|
3059
|
+
// Temporarily stage untracked files with --intent-to-add.
|
|
3060
|
+
if (untrackedFiles.length > 0) {
|
|
3061
|
+
try {
|
|
3062
|
+
execFileSync("git", ["add", "--intent-to-add", ...untrackedFiles], {
|
|
3063
|
+
cwd,
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
catch {
|
|
3067
|
+
// Ignore staging errors.
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
execFile("git", ["diff", "--no-color"], execOpts, (err, stdout) => {
|
|
3071
|
+
// Revert intent-to-add for untracked files.
|
|
3072
|
+
if (untrackedFiles.length > 0) {
|
|
3073
|
+
try {
|
|
3074
|
+
execFileSync("git", ["reset", "--", ...untrackedFiles], { cwd });
|
|
3075
|
+
}
|
|
3076
|
+
catch {
|
|
3077
|
+
// Ignore reset errors.
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
if (err) {
|
|
3081
|
+
callback({ diff: "", error: err.message });
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
callback({ diff: stdout });
|
|
3085
|
+
});
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
// All mode (no options): git diff HEAD — shows both staged and unstaged vs HEAD
|
|
3089
|
+
let untrackedFilesAll = [];
|
|
2327
3090
|
try {
|
|
2328
|
-
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd })
|
|
2329
|
-
|
|
3091
|
+
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd })
|
|
3092
|
+
.toString()
|
|
3093
|
+
.trim();
|
|
3094
|
+
untrackedFilesAll = out ? out.split("\n") : [];
|
|
2330
3095
|
}
|
|
2331
3096
|
catch {
|
|
2332
|
-
// Ignore
|
|
3097
|
+
// Ignore
|
|
2333
3098
|
}
|
|
2334
|
-
|
|
2335
|
-
if (untrackedFiles.length > 0) {
|
|
3099
|
+
if (untrackedFilesAll.length > 0) {
|
|
2336
3100
|
try {
|
|
2337
|
-
execFileSync("git", ["add", "--intent-to-add", ...
|
|
3101
|
+
execFileSync("git", ["add", "--intent-to-add", ...untrackedFilesAll], {
|
|
3102
|
+
cwd,
|
|
3103
|
+
});
|
|
2338
3104
|
}
|
|
2339
3105
|
catch {
|
|
2340
|
-
// Ignore
|
|
3106
|
+
// Ignore
|
|
2341
3107
|
}
|
|
2342
3108
|
}
|
|
2343
|
-
execFile("git", ["diff", "--no-color"], execOpts, (err, stdout) => {
|
|
2344
|
-
|
|
2345
|
-
if (untrackedFiles.length > 0) {
|
|
3109
|
+
execFile("git", ["diff", "HEAD", "--no-color"], execOpts, (err, stdout) => {
|
|
3110
|
+
if (untrackedFilesAll.length > 0) {
|
|
2346
3111
|
try {
|
|
2347
|
-
execFileSync("git", ["reset", "--", ...
|
|
3112
|
+
execFileSync("git", ["reset", "--", ...untrackedFilesAll], { cwd });
|
|
2348
3113
|
}
|
|
2349
3114
|
catch {
|
|
2350
|
-
// Ignore
|
|
3115
|
+
// Ignore
|
|
2351
3116
|
}
|
|
2352
3117
|
}
|
|
2353
3118
|
if (err) {
|
|
@@ -2361,7 +3126,14 @@ export class BridgeWebSocketServer {
|
|
|
2361
3126
|
// Image diff helpers
|
|
2362
3127
|
// ---------------------------------------------------------------------------
|
|
2363
3128
|
static IMAGE_EXTENSIONS = new Set([
|
|
2364
|
-
".png",
|
|
3129
|
+
".png",
|
|
3130
|
+
".jpg",
|
|
3131
|
+
".jpeg",
|
|
3132
|
+
".gif",
|
|
3133
|
+
".webp",
|
|
3134
|
+
".ico",
|
|
3135
|
+
".bmp",
|
|
3136
|
+
".svg",
|
|
2365
3137
|
]);
|
|
2366
3138
|
// Image diff thresholds (configurable via environment variables)
|
|
2367
3139
|
// - Auto-display: images ≤ threshold are sent inline as base64
|
|
@@ -2535,7 +3307,9 @@ export class BridgeWebSocketServer {
|
|
|
2535
3307
|
}
|
|
2536
3308
|
}
|
|
2537
3309
|
extractSessionIdFromClientMessage(msg) {
|
|
2538
|
-
return "sessionId" in msg && typeof msg.sessionId === "string"
|
|
3310
|
+
return "sessionId" in msg && typeof msg.sessionId === "string"
|
|
3311
|
+
? msg.sessionId
|
|
3312
|
+
: undefined;
|
|
2539
3313
|
}
|
|
2540
3314
|
extractSessionIdFromServerMessage(msg) {
|
|
2541
3315
|
if ("sessionId" in msg && typeof msg.sessionId === "string")
|
|
@@ -2564,8 +3338,7 @@ export class BridgeWebSocketServer {
|
|
|
2564
3338
|
return events.slice(-capped);
|
|
2565
3339
|
}
|
|
2566
3340
|
buildHistorySummary(history) {
|
|
2567
|
-
const lines = history
|
|
2568
|
-
.map((msg, index) => {
|
|
3341
|
+
const lines = history.map((msg, index) => {
|
|
2569
3342
|
const num = String(index + 1).padStart(3, "0");
|
|
2570
3343
|
return `${num}. ${this.summarizeServerMessage(msg)}`;
|
|
2571
3344
|
});
|
|
@@ -2620,7 +3393,10 @@ export class BridgeWebSocketServer {
|
|
|
2620
3393
|
return text ? `assistant: ${text}` : "assistant";
|
|
2621
3394
|
}
|
|
2622
3395
|
case "tool_result": {
|
|
2623
|
-
const contentPreview = msg.content
|
|
3396
|
+
const contentPreview = msg.content
|
|
3397
|
+
.replace(/\s+/g, " ")
|
|
3398
|
+
.trim()
|
|
3399
|
+
.slice(0, 100);
|
|
2624
3400
|
return `${msg.toolName ?? "tool_result"}(${msg.toolUseId}) ${contentPreview}`;
|
|
2625
3401
|
}
|
|
2626
3402
|
case "permission_request":
|