@ccpocket/bridge 1.29.1 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +138 -0
- package/dist/parser.js +194 -46
- package/dist/parser.js.map +1 -1
- package/dist/websocket.js +1022 -202
- 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
|
}
|
|
@@ -1490,6 +1674,93 @@ export class BridgeWebSocketServer {
|
|
|
1490
1674
|
this.send(ws, { type: "project_history", projects });
|
|
1491
1675
|
break;
|
|
1492
1676
|
}
|
|
1677
|
+
case "read_file": {
|
|
1678
|
+
const absPath = resolve(msg.projectPath, msg.filePath);
|
|
1679
|
+
if (!this.isPathAllowed(absPath)) {
|
|
1680
|
+
this.send(ws, {
|
|
1681
|
+
type: "file_content",
|
|
1682
|
+
filePath: msg.filePath,
|
|
1683
|
+
content: "",
|
|
1684
|
+
error: "Path not allowed",
|
|
1685
|
+
});
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
void (async () => {
|
|
1689
|
+
try {
|
|
1690
|
+
if (!existsSync(absPath)) {
|
|
1691
|
+
this.send(ws, {
|
|
1692
|
+
type: "file_content",
|
|
1693
|
+
filePath: msg.filePath,
|
|
1694
|
+
content: "",
|
|
1695
|
+
error: "File not found",
|
|
1696
|
+
});
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
|
|
1700
|
+
? msg.maxLines
|
|
1701
|
+
: 5000;
|
|
1702
|
+
const raw = await readFile(absPath, "utf-8");
|
|
1703
|
+
const ext = extname(absPath).replace(/^\./, "").toLowerCase();
|
|
1704
|
+
const languageMap = {
|
|
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",
|
|
1737
|
+
};
|
|
1738
|
+
const language = languageMap[ext] ?? (ext || undefined);
|
|
1739
|
+
const lines = raw.split("\n");
|
|
1740
|
+
const truncated = lines.length > maxLines;
|
|
1741
|
+
const content = truncated
|
|
1742
|
+
? lines.slice(0, maxLines).join("\n")
|
|
1743
|
+
: raw;
|
|
1744
|
+
this.send(ws, {
|
|
1745
|
+
type: "file_content",
|
|
1746
|
+
filePath: msg.filePath,
|
|
1747
|
+
content,
|
|
1748
|
+
language,
|
|
1749
|
+
totalLines: lines.length,
|
|
1750
|
+
truncated,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
this.send(ws, {
|
|
1755
|
+
type: "file_content",
|
|
1756
|
+
filePath: msg.filePath,
|
|
1757
|
+
content: "",
|
|
1758
|
+
error: `Failed to read file: ${err}`,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
})();
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1493
1764
|
case "list_files": {
|
|
1494
1765
|
if (!this.isPathAllowed(msg.projectPath)) {
|
|
1495
1766
|
this.send(ws, this.buildPathNotAllowedError(msg.projectPath));
|
|
@@ -1502,7 +1773,10 @@ export class BridgeWebSocketServer {
|
|
|
1502
1773
|
this.send(ws, { type: "file_list", files: [] });
|
|
1503
1774
|
}
|
|
1504
1775
|
else {
|
|
1505
|
-
this.send(ws, {
|
|
1776
|
+
this.send(ws, {
|
|
1777
|
+
type: "error",
|
|
1778
|
+
message: `Failed to list files: ${err.message}`,
|
|
1779
|
+
});
|
|
1506
1780
|
}
|
|
1507
1781
|
return;
|
|
1508
1782
|
}
|
|
@@ -1568,15 +1842,27 @@ export class BridgeWebSocketServer {
|
|
|
1568
1842
|
}
|
|
1569
1843
|
case "get_recording": {
|
|
1570
1844
|
if (!this.recordingStore) {
|
|
1571
|
-
this.send(ws, {
|
|
1845
|
+
this.send(ws, {
|
|
1846
|
+
type: "error",
|
|
1847
|
+
message: "Recording is not enabled on this server",
|
|
1848
|
+
});
|
|
1572
1849
|
break;
|
|
1573
1850
|
}
|
|
1574
|
-
void this.recordingStore
|
|
1851
|
+
void this.recordingStore
|
|
1852
|
+
.getRecordingContent(msg.sessionId)
|
|
1853
|
+
.then((content) => {
|
|
1575
1854
|
if (content !== null) {
|
|
1576
|
-
this.send(ws, {
|
|
1855
|
+
this.send(ws, {
|
|
1856
|
+
type: "recording_content",
|
|
1857
|
+
sessionId: msg.sessionId,
|
|
1858
|
+
content,
|
|
1859
|
+
});
|
|
1577
1860
|
}
|
|
1578
1861
|
else {
|
|
1579
|
-
this.send(ws, {
|
|
1862
|
+
this.send(ws, {
|
|
1863
|
+
type: "error",
|
|
1864
|
+
message: `Recording ${msg.sessionId} not found`,
|
|
1865
|
+
});
|
|
1580
1866
|
}
|
|
1581
1867
|
});
|
|
1582
1868
|
break;
|
|
@@ -1597,7 +1883,11 @@ export class BridgeWebSocketServer {
|
|
|
1597
1883
|
});
|
|
1598
1884
|
}
|
|
1599
1885
|
else {
|
|
1600
|
-
this.send(ws, {
|
|
1886
|
+
this.send(ws, {
|
|
1887
|
+
type: "diff_result",
|
|
1888
|
+
diff: "",
|
|
1889
|
+
error: `Failed to get diff: ${error}`,
|
|
1890
|
+
});
|
|
1601
1891
|
}
|
|
1602
1892
|
return;
|
|
1603
1893
|
}
|
|
@@ -1609,11 +1899,16 @@ export class BridgeWebSocketServer {
|
|
|
1609
1899
|
this.send(ws, { type: "diff_result", diff });
|
|
1610
1900
|
}
|
|
1611
1901
|
});
|
|
1612
|
-
}
|
|
1902
|
+
}, msg.staged === true
|
|
1903
|
+
? { staged: true }
|
|
1904
|
+
: msg.staged === false
|
|
1905
|
+
? { unstaged: true }
|
|
1906
|
+
: undefined);
|
|
1613
1907
|
break;
|
|
1614
1908
|
}
|
|
1615
1909
|
case "get_diff_image": {
|
|
1616
|
-
if (!this.isPathAllowed(msg.projectPath) ||
|
|
1910
|
+
if (!this.isPathAllowed(msg.projectPath) ||
|
|
1911
|
+
!this.isPathAllowed(resolve(msg.projectPath, msg.filePath))) {
|
|
1617
1912
|
this.send(ws, { type: "error", message: `Path not allowed` });
|
|
1618
1913
|
break;
|
|
1619
1914
|
}
|
|
@@ -1645,7 +1940,12 @@ export class BridgeWebSocketServer {
|
|
|
1645
1940
|
void (async () => {
|
|
1646
1941
|
try {
|
|
1647
1942
|
const result = await this.loadDiffImageAsync(msg.projectPath, msg.filePath, version);
|
|
1648
|
-
this.send(ws, {
|
|
1943
|
+
this.send(ws, {
|
|
1944
|
+
type: "diff_image_result",
|
|
1945
|
+
filePath: msg.filePath,
|
|
1946
|
+
version,
|
|
1947
|
+
...result,
|
|
1948
|
+
});
|
|
1649
1949
|
}
|
|
1650
1950
|
catch {
|
|
1651
1951
|
// WebSocket may have closed; ignore send errors.
|
|
@@ -1665,7 +1965,10 @@ export class BridgeWebSocketServer {
|
|
|
1665
1965
|
this.send(ws, { type: "worktree_list", worktrees, mainBranch });
|
|
1666
1966
|
}
|
|
1667
1967
|
catch (err) {
|
|
1668
|
-
this.send(ws, {
|
|
1968
|
+
this.send(ws, {
|
|
1969
|
+
type: "error",
|
|
1970
|
+
message: `Failed to list worktrees: ${err}`,
|
|
1971
|
+
});
|
|
1669
1972
|
}
|
|
1670
1973
|
break;
|
|
1671
1974
|
}
|
|
@@ -1677,20 +1980,334 @@ export class BridgeWebSocketServer {
|
|
|
1677
1980
|
try {
|
|
1678
1981
|
removeWorktree(msg.projectPath, msg.worktreePath);
|
|
1679
1982
|
this.worktreeStore.deleteByWorktreePath(msg.worktreePath);
|
|
1680
|
-
this.send(ws, {
|
|
1983
|
+
this.send(ws, {
|
|
1984
|
+
type: "worktree_removed",
|
|
1985
|
+
worktreePath: msg.worktreePath,
|
|
1986
|
+
});
|
|
1681
1987
|
}
|
|
1682
1988
|
catch (err) {
|
|
1683
|
-
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
|
+
});
|
|
1684
2295
|
}
|
|
1685
2296
|
break;
|
|
1686
2297
|
}
|
|
1687
2298
|
case "rewind_dry_run": {
|
|
1688
2299
|
const session = this.sessionManager.get(msg.sessionId);
|
|
1689
2300
|
if (!session) {
|
|
1690
|
-
this.send(ws, {
|
|
2301
|
+
this.send(ws, {
|
|
2302
|
+
type: "rewind_preview",
|
|
2303
|
+
canRewind: false,
|
|
2304
|
+
error: `Session ${msg.sessionId} not found`,
|
|
2305
|
+
});
|
|
1691
2306
|
return;
|
|
1692
2307
|
}
|
|
1693
|
-
this.sessionManager
|
|
2308
|
+
this.sessionManager
|
|
2309
|
+
.rewindFiles(msg.sessionId, msg.targetUuid, true)
|
|
2310
|
+
.then((result) => {
|
|
1694
2311
|
this.send(ws, {
|
|
1695
2312
|
type: "rewind_preview",
|
|
1696
2313
|
canRewind: result.canRewind,
|
|
@@ -1699,40 +2316,73 @@ export class BridgeWebSocketServer {
|
|
|
1699
2316
|
deletions: result.deletions,
|
|
1700
2317
|
error: result.error,
|
|
1701
2318
|
});
|
|
1702
|
-
})
|
|
1703
|
-
|
|
2319
|
+
})
|
|
2320
|
+
.catch((err) => {
|
|
2321
|
+
this.send(ws, {
|
|
2322
|
+
type: "rewind_preview",
|
|
2323
|
+
canRewind: false,
|
|
2324
|
+
error: `Dry run failed: ${err}`,
|
|
2325
|
+
});
|
|
1704
2326
|
});
|
|
1705
2327
|
break;
|
|
1706
2328
|
}
|
|
1707
2329
|
case "rewind": {
|
|
1708
2330
|
const session = this.sessionManager.get(msg.sessionId);
|
|
1709
2331
|
if (!session) {
|
|
1710
|
-
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
|
+
});
|
|
1711
2338
|
return;
|
|
1712
2339
|
}
|
|
1713
2340
|
const handleError = (err) => {
|
|
1714
2341
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1715
|
-
this.send(ws, {
|
|
2342
|
+
this.send(ws, {
|
|
2343
|
+
type: "rewind_result",
|
|
2344
|
+
success: false,
|
|
2345
|
+
mode: msg.mode,
|
|
2346
|
+
error: errMsg,
|
|
2347
|
+
});
|
|
1716
2348
|
};
|
|
1717
2349
|
if (msg.mode === "code") {
|
|
1718
2350
|
// Code-only rewind: rewind files without restarting the conversation
|
|
1719
|
-
this.sessionManager
|
|
2351
|
+
this.sessionManager
|
|
2352
|
+
.rewindFiles(msg.sessionId, msg.targetUuid)
|
|
2353
|
+
.then((result) => {
|
|
1720
2354
|
if (result.canRewind) {
|
|
1721
|
-
this.send(ws, {
|
|
2355
|
+
this.send(ws, {
|
|
2356
|
+
type: "rewind_result",
|
|
2357
|
+
success: true,
|
|
2358
|
+
mode: "code",
|
|
2359
|
+
});
|
|
1722
2360
|
}
|
|
1723
2361
|
else {
|
|
1724
|
-
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
|
+
});
|
|
1725
2368
|
}
|
|
1726
|
-
})
|
|
2369
|
+
})
|
|
2370
|
+
.catch(handleError);
|
|
1727
2371
|
}
|
|
1728
2372
|
else if (msg.mode === "conversation") {
|
|
1729
2373
|
// Conversation-only rewind: restart session at the target UUID
|
|
1730
2374
|
try {
|
|
1731
2375
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
1732
|
-
this.send(ws, {
|
|
2376
|
+
this.send(ws, {
|
|
2377
|
+
type: "rewind_result",
|
|
2378
|
+
success: true,
|
|
2379
|
+
mode: "conversation",
|
|
2380
|
+
});
|
|
1733
2381
|
// Notify the new session ID
|
|
1734
2382
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1735
|
-
const rewindPermMode = newSession?.process instanceof SdkProcess
|
|
2383
|
+
const rewindPermMode = newSession?.process instanceof SdkProcess
|
|
2384
|
+
? newSession.process.permissionMode
|
|
2385
|
+
: undefined;
|
|
1736
2386
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1737
2387
|
sessionId: newSessionId,
|
|
1738
2388
|
provider: newSession?.provider ?? "claude",
|
|
@@ -1750,16 +2400,29 @@ export class BridgeWebSocketServer {
|
|
|
1750
2400
|
}
|
|
1751
2401
|
else {
|
|
1752
2402
|
// Both: rewind files first, then rewind conversation
|
|
1753
|
-
this.sessionManager
|
|
2403
|
+
this.sessionManager
|
|
2404
|
+
.rewindFiles(msg.sessionId, msg.targetUuid)
|
|
2405
|
+
.then((result) => {
|
|
1754
2406
|
if (!result.canRewind) {
|
|
1755
|
-
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
|
+
});
|
|
1756
2413
|
return;
|
|
1757
2414
|
}
|
|
1758
2415
|
try {
|
|
1759
2416
|
this.sessionManager.rewindConversation(msg.sessionId, msg.targetUuid, (newSessionId) => {
|
|
1760
|
-
this.send(ws, {
|
|
2417
|
+
this.send(ws, {
|
|
2418
|
+
type: "rewind_result",
|
|
2419
|
+
success: true,
|
|
2420
|
+
mode: "both",
|
|
2421
|
+
});
|
|
1761
2422
|
const newSession = this.sessionManager.get(newSessionId);
|
|
1762
|
-
const rewindPermMode2 = newSession?.process instanceof SdkProcess
|
|
2423
|
+
const rewindPermMode2 = newSession?.process instanceof SdkProcess
|
|
2424
|
+
? newSession.process.permissionMode
|
|
2425
|
+
: undefined;
|
|
1763
2426
|
this.send(ws, this.buildSessionCreatedMessage({
|
|
1764
2427
|
sessionId: newSessionId,
|
|
1765
2428
|
provider: newSession?.provider ?? "claude",
|
|
@@ -1774,7 +2437,8 @@ export class BridgeWebSocketServer {
|
|
|
1774
2437
|
catch (err) {
|
|
1775
2438
|
handleError(err);
|
|
1776
2439
|
}
|
|
1777
|
-
})
|
|
2440
|
+
})
|
|
2441
|
+
.catch(handleError);
|
|
1778
2442
|
}
|
|
1779
2443
|
break;
|
|
1780
2444
|
}
|
|
@@ -1815,7 +2479,11 @@ export class BridgeWebSocketServer {
|
|
|
1815
2479
|
const meta = await this.galleryStore.addImage(result.filePath, msg.projectPath, msg.sessionId);
|
|
1816
2480
|
if (meta) {
|
|
1817
2481
|
const info = this.galleryStore.metaToInfo(meta);
|
|
1818
|
-
this.send(ws, {
|
|
2482
|
+
this.send(ws, {
|
|
2483
|
+
type: "screenshot_result",
|
|
2484
|
+
success: true,
|
|
2485
|
+
image: info,
|
|
2486
|
+
});
|
|
1819
2487
|
this.broadcast({ type: "gallery_new_image", image: info });
|
|
1820
2488
|
return;
|
|
1821
2489
|
}
|
|
@@ -1842,23 +2510,44 @@ export class BridgeWebSocketServer {
|
|
|
1842
2510
|
}
|
|
1843
2511
|
case "backup_prompt_history": {
|
|
1844
2512
|
if (!this.promptHistoryBackup) {
|
|
1845
|
-
this.send(ws, {
|
|
2513
|
+
this.send(ws, {
|
|
2514
|
+
type: "prompt_history_backup_result",
|
|
2515
|
+
success: false,
|
|
2516
|
+
error: "Backup store not available",
|
|
2517
|
+
});
|
|
1846
2518
|
break;
|
|
1847
2519
|
}
|
|
1848
2520
|
const buf = Buffer.from(msg.data, "base64");
|
|
1849
|
-
this.promptHistoryBackup
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
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
|
+
});
|
|
1853
2536
|
});
|
|
1854
2537
|
break;
|
|
1855
2538
|
}
|
|
1856
2539
|
case "restore_prompt_history": {
|
|
1857
2540
|
if (!this.promptHistoryBackup) {
|
|
1858
|
-
this.send(ws, {
|
|
2541
|
+
this.send(ws, {
|
|
2542
|
+
type: "prompt_history_restore_result",
|
|
2543
|
+
success: false,
|
|
2544
|
+
error: "Backup store not available",
|
|
2545
|
+
});
|
|
1859
2546
|
break;
|
|
1860
2547
|
}
|
|
1861
|
-
this.promptHistoryBackup
|
|
2548
|
+
this.promptHistoryBackup
|
|
2549
|
+
.load()
|
|
2550
|
+
.then((result) => {
|
|
1862
2551
|
if (result) {
|
|
1863
2552
|
this.send(ws, {
|
|
1864
2553
|
type: "prompt_history_restore_result",
|
|
@@ -1870,10 +2559,19 @@ export class BridgeWebSocketServer {
|
|
|
1870
2559
|
});
|
|
1871
2560
|
}
|
|
1872
2561
|
else {
|
|
1873
|
-
this.send(ws, {
|
|
2562
|
+
this.send(ws, {
|
|
2563
|
+
type: "prompt_history_restore_result",
|
|
2564
|
+
success: false,
|
|
2565
|
+
error: "No backup found",
|
|
2566
|
+
});
|
|
1874
2567
|
}
|
|
1875
|
-
})
|
|
1876
|
-
|
|
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
|
+
});
|
|
1877
2575
|
});
|
|
1878
2576
|
break;
|
|
1879
2577
|
}
|
|
@@ -1882,15 +2580,28 @@ export class BridgeWebSocketServer {
|
|
|
1882
2580
|
this.send(ws, { type: "prompt_history_backup_info", exists: false });
|
|
1883
2581
|
break;
|
|
1884
2582
|
}
|
|
1885
|
-
this.promptHistoryBackup
|
|
2583
|
+
this.promptHistoryBackup
|
|
2584
|
+
.getMeta()
|
|
2585
|
+
.then((meta) => {
|
|
1886
2586
|
if (meta) {
|
|
1887
|
-
this.send(ws, {
|
|
2587
|
+
this.send(ws, {
|
|
2588
|
+
type: "prompt_history_backup_info",
|
|
2589
|
+
exists: true,
|
|
2590
|
+
...meta,
|
|
2591
|
+
});
|
|
1888
2592
|
}
|
|
1889
2593
|
else {
|
|
1890
|
-
this.send(ws, {
|
|
2594
|
+
this.send(ws, {
|
|
2595
|
+
type: "prompt_history_backup_info",
|
|
2596
|
+
exists: false,
|
|
2597
|
+
});
|
|
1891
2598
|
}
|
|
1892
|
-
})
|
|
1893
|
-
|
|
2599
|
+
})
|
|
2600
|
+
.catch(() => {
|
|
2601
|
+
this.send(ws, {
|
|
2602
|
+
type: "prompt_history_backup_info",
|
|
2603
|
+
exists: false,
|
|
2604
|
+
});
|
|
1894
2605
|
});
|
|
1895
2606
|
break;
|
|
1896
2607
|
}
|
|
@@ -1937,10 +2648,12 @@ export class BridgeWebSocketServer {
|
|
|
1937
2648
|
if (runningSession) {
|
|
1938
2649
|
this.sessionManager.renameSession(sessionId, name);
|
|
1939
2650
|
// Persist to provider storage
|
|
1940
|
-
if (runningSession.provider === "claude" &&
|
|
2651
|
+
if (runningSession.provider === "claude" &&
|
|
2652
|
+
runningSession.claudeSessionId) {
|
|
1941
2653
|
await renameClaudeSession(runningSession.worktreePath ?? runningSession.projectPath, runningSession.claudeSessionId, name);
|
|
1942
2654
|
}
|
|
1943
|
-
else if (runningSession.provider === "codex" &&
|
|
2655
|
+
else if (runningSession.provider === "codex" &&
|
|
2656
|
+
runningSession.process) {
|
|
1944
2657
|
try {
|
|
1945
2658
|
await runningSession.process.renameThread(name ?? "");
|
|
1946
2659
|
}
|
|
@@ -1984,13 +2697,27 @@ export class BridgeWebSocketServer {
|
|
|
1984
2697
|
sendSessionList(ws) {
|
|
1985
2698
|
this.pruneDebugEvents();
|
|
1986
2699
|
const sessions = this.sessionManager.list();
|
|
1987
|
-
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
|
+
});
|
|
1988
2708
|
}
|
|
1989
2709
|
/** Broadcast session list to all connected clients. */
|
|
1990
2710
|
broadcastSessionList() {
|
|
1991
2711
|
this.pruneDebugEvents();
|
|
1992
2712
|
const sessions = this.sessionManager.list();
|
|
1993
|
-
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
|
+
});
|
|
1994
2721
|
}
|
|
1995
2722
|
broadcastSessionMessage(sessionId, msg) {
|
|
1996
2723
|
this.maybeSendPushNotification(sessionId, msg);
|
|
@@ -2002,7 +2729,9 @@ export class BridgeWebSocketServer {
|
|
|
2002
2729
|
});
|
|
2003
2730
|
this.recordingStore?.record(sessionId, "outgoing", msg);
|
|
2004
2731
|
// Update recording meta with claudeSessionId when it becomes available
|
|
2005
|
-
if ((msg.type === "system" || msg.type === "result") &&
|
|
2732
|
+
if ((msg.type === "system" || msg.type === "result") &&
|
|
2733
|
+
"sessionId" in msg &&
|
|
2734
|
+
msg.sessionId) {
|
|
2006
2735
|
const session = this.sessionManager.get(sessionId);
|
|
2007
2736
|
if (session) {
|
|
2008
2737
|
this.recordingStore?.saveMeta(sessionId, {
|
|
@@ -2041,16 +2770,21 @@ export class BridgeWebSocketServer {
|
|
|
2041
2770
|
});
|
|
2042
2771
|
}
|
|
2043
2772
|
getActiveCodexProcess() {
|
|
2044
|
-
const summary = this.sessionManager
|
|
2773
|
+
const summary = this.sessionManager
|
|
2774
|
+
.list()
|
|
2775
|
+
.find((session) => session.provider === "codex");
|
|
2045
2776
|
if (!summary)
|
|
2046
2777
|
return null;
|
|
2047
2778
|
const session = this.sessionManager.get(summary.id);
|
|
2048
|
-
return session?.provider === "codex"
|
|
2779
|
+
return session?.provider === "codex"
|
|
2780
|
+
? session.process
|
|
2781
|
+
: null;
|
|
2049
2782
|
}
|
|
2050
2783
|
async listRecentCodexThreads(msg) {
|
|
2051
2784
|
const limit = msg.limit ?? 20;
|
|
2052
2785
|
const offset = msg.offset ?? 0;
|
|
2053
|
-
const process = this.getActiveCodexProcess() ??
|
|
2786
|
+
const process = this.getActiveCodexProcess() ??
|
|
2787
|
+
(await this.createStandaloneCodexProcess(msg.projectPath));
|
|
2054
2788
|
const isStandalone = process !== this.getActiveCodexProcess();
|
|
2055
2789
|
try {
|
|
2056
2790
|
const result = await process.listThreads({
|
|
@@ -2135,7 +2869,9 @@ export class BridgeWebSocketServer {
|
|
|
2135
2869
|
this.notifiedPermissionToolUses.set(sessionId, seen);
|
|
2136
2870
|
const isAskUserQuestion = msg.toolName === "AskUserQuestion";
|
|
2137
2871
|
const isExitPlanMode = msg.toolName === "ExitPlanMode";
|
|
2138
|
-
const eventType = isAskUserQuestion
|
|
2872
|
+
const eventType = isAskUserQuestion
|
|
2873
|
+
? "ask_user_question"
|
|
2874
|
+
: "approval_required";
|
|
2139
2875
|
// Extract question text for AskUserQuestion (standard mode only)
|
|
2140
2876
|
let questionText;
|
|
2141
2877
|
if (!privacy && isAskUserQuestion) {
|
|
@@ -2158,30 +2894,38 @@ export class BridgeWebSocketServer {
|
|
|
2158
2894
|
let body;
|
|
2159
2895
|
if (isExitPlanMode) {
|
|
2160
2896
|
const titleKey = "plan_ready_title";
|
|
2161
|
-
title = label
|
|
2897
|
+
title = label
|
|
2898
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2899
|
+
: t(locale, titleKey);
|
|
2162
2900
|
body = t(locale, "plan_ready_body");
|
|
2163
2901
|
}
|
|
2164
2902
|
else if (isAskUserQuestion) {
|
|
2165
2903
|
const titleKey = "ask_title";
|
|
2166
|
-
title = label
|
|
2904
|
+
title = label
|
|
2905
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2906
|
+
: t(locale, titleKey);
|
|
2167
2907
|
body = privacy
|
|
2168
2908
|
? t(locale, "ask_body_private")
|
|
2169
2909
|
: (questionText ?? t(locale, "ask_default_body"));
|
|
2170
2910
|
}
|
|
2171
2911
|
else {
|
|
2172
2912
|
const titleKey = "approval_title";
|
|
2173
|
-
title = label
|
|
2913
|
+
title = label
|
|
2914
|
+
? `${t(locale, titleKey)} - ${label}`
|
|
2915
|
+
: t(locale, titleKey);
|
|
2174
2916
|
body = privacy
|
|
2175
2917
|
? t(locale, "approval_body_private")
|
|
2176
2918
|
: t(locale, "approval_body", { toolName: msg.toolName });
|
|
2177
2919
|
}
|
|
2178
|
-
void this.pushRelay
|
|
2920
|
+
void this.pushRelay
|
|
2921
|
+
.notify({
|
|
2179
2922
|
eventType,
|
|
2180
2923
|
title,
|
|
2181
2924
|
body,
|
|
2182
2925
|
locale,
|
|
2183
2926
|
data,
|
|
2184
|
-
})
|
|
2927
|
+
})
|
|
2928
|
+
.catch((err) => {
|
|
2185
2929
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2186
2930
|
console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
|
|
2187
2931
|
});
|
|
@@ -2216,12 +2960,18 @@ export class BridgeWebSocketServer {
|
|
|
2216
2960
|
for (const locale of this.getRegisteredLocales()) {
|
|
2217
2961
|
let title;
|
|
2218
2962
|
if (privacy) {
|
|
2219
|
-
title = isSuccess
|
|
2963
|
+
title = isSuccess
|
|
2964
|
+
? t(locale, "task_completed")
|
|
2965
|
+
: t(locale, "error_occurred");
|
|
2220
2966
|
}
|
|
2221
2967
|
else {
|
|
2222
2968
|
title = label
|
|
2223
|
-
?
|
|
2224
|
-
|
|
2969
|
+
? isSuccess
|
|
2970
|
+
? `✅ ${label}`
|
|
2971
|
+
: `❌ ${label}`
|
|
2972
|
+
: isSuccess
|
|
2973
|
+
? t(locale, "task_completed")
|
|
2974
|
+
: t(locale, "error_occurred");
|
|
2225
2975
|
}
|
|
2226
2976
|
let body;
|
|
2227
2977
|
if (privacy) {
|
|
@@ -2236,15 +2986,19 @@ export class BridgeWebSocketServer {
|
|
|
2236
2986
|
: `${t(locale, "session_completed")}${stats}`;
|
|
2237
2987
|
}
|
|
2238
2988
|
else {
|
|
2239
|
-
body = msg.error
|
|
2989
|
+
body = msg.error
|
|
2990
|
+
? msg.error.slice(0, 120)
|
|
2991
|
+
: t(locale, "session_failed");
|
|
2240
2992
|
}
|
|
2241
|
-
void this.pushRelay
|
|
2993
|
+
void this.pushRelay
|
|
2994
|
+
.notify({
|
|
2242
2995
|
eventType,
|
|
2243
2996
|
title,
|
|
2244
2997
|
body,
|
|
2245
2998
|
locale,
|
|
2246
2999
|
data,
|
|
2247
|
-
})
|
|
3000
|
+
})
|
|
3001
|
+
.catch((err) => {
|
|
2248
3002
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2249
3003
|
console.warn(`[ws] Failed to send push notification (${eventType}, ${locale}): ${detail}`);
|
|
2250
3004
|
});
|
|
@@ -2276,34 +3030,89 @@ export class BridgeWebSocketServer {
|
|
|
2276
3030
|
broadcastGalleryNewImage(image) {
|
|
2277
3031
|
this.broadcast({ type: "gallery_new_image", image });
|
|
2278
3032
|
}
|
|
2279
|
-
collectGitDiff(cwd, callback) {
|
|
3033
|
+
collectGitDiff(cwd, callback, options) {
|
|
2280
3034
|
const execOpts = { cwd, maxBuffer: 10 * 1024 * 1024 };
|
|
2281
|
-
//
|
|
2282
|
-
|
|
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 = [];
|
|
2283
3090
|
try {
|
|
2284
|
-
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd })
|
|
2285
|
-
|
|
3091
|
+
const out = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd })
|
|
3092
|
+
.toString()
|
|
3093
|
+
.trim();
|
|
3094
|
+
untrackedFilesAll = out ? out.split("\n") : [];
|
|
2286
3095
|
}
|
|
2287
3096
|
catch {
|
|
2288
|
-
// Ignore
|
|
3097
|
+
// Ignore
|
|
2289
3098
|
}
|
|
2290
|
-
|
|
2291
|
-
if (untrackedFiles.length > 0) {
|
|
3099
|
+
if (untrackedFilesAll.length > 0) {
|
|
2292
3100
|
try {
|
|
2293
|
-
execFileSync("git", ["add", "--intent-to-add", ...
|
|
3101
|
+
execFileSync("git", ["add", "--intent-to-add", ...untrackedFilesAll], {
|
|
3102
|
+
cwd,
|
|
3103
|
+
});
|
|
2294
3104
|
}
|
|
2295
3105
|
catch {
|
|
2296
|
-
// Ignore
|
|
3106
|
+
// Ignore
|
|
2297
3107
|
}
|
|
2298
3108
|
}
|
|
2299
|
-
execFile("git", ["diff", "--no-color"], execOpts, (err, stdout) => {
|
|
2300
|
-
|
|
2301
|
-
if (untrackedFiles.length > 0) {
|
|
3109
|
+
execFile("git", ["diff", "HEAD", "--no-color"], execOpts, (err, stdout) => {
|
|
3110
|
+
if (untrackedFilesAll.length > 0) {
|
|
2302
3111
|
try {
|
|
2303
|
-
execFileSync("git", ["reset", "--", ...
|
|
3112
|
+
execFileSync("git", ["reset", "--", ...untrackedFilesAll], { cwd });
|
|
2304
3113
|
}
|
|
2305
3114
|
catch {
|
|
2306
|
-
// Ignore
|
|
3115
|
+
// Ignore
|
|
2307
3116
|
}
|
|
2308
3117
|
}
|
|
2309
3118
|
if (err) {
|
|
@@ -2317,7 +3126,14 @@ export class BridgeWebSocketServer {
|
|
|
2317
3126
|
// Image diff helpers
|
|
2318
3127
|
// ---------------------------------------------------------------------------
|
|
2319
3128
|
static IMAGE_EXTENSIONS = new Set([
|
|
2320
|
-
".png",
|
|
3129
|
+
".png",
|
|
3130
|
+
".jpg",
|
|
3131
|
+
".jpeg",
|
|
3132
|
+
".gif",
|
|
3133
|
+
".webp",
|
|
3134
|
+
".ico",
|
|
3135
|
+
".bmp",
|
|
3136
|
+
".svg",
|
|
2321
3137
|
]);
|
|
2322
3138
|
// Image diff thresholds (configurable via environment variables)
|
|
2323
3139
|
// - Auto-display: images ≤ threshold are sent inline as base64
|
|
@@ -2491,7 +3307,9 @@ export class BridgeWebSocketServer {
|
|
|
2491
3307
|
}
|
|
2492
3308
|
}
|
|
2493
3309
|
extractSessionIdFromClientMessage(msg) {
|
|
2494
|
-
return "sessionId" in msg && typeof msg.sessionId === "string"
|
|
3310
|
+
return "sessionId" in msg && typeof msg.sessionId === "string"
|
|
3311
|
+
? msg.sessionId
|
|
3312
|
+
: undefined;
|
|
2495
3313
|
}
|
|
2496
3314
|
extractSessionIdFromServerMessage(msg) {
|
|
2497
3315
|
if ("sessionId" in msg && typeof msg.sessionId === "string")
|
|
@@ -2520,8 +3338,7 @@ export class BridgeWebSocketServer {
|
|
|
2520
3338
|
return events.slice(-capped);
|
|
2521
3339
|
}
|
|
2522
3340
|
buildHistorySummary(history) {
|
|
2523
|
-
const lines = history
|
|
2524
|
-
.map((msg, index) => {
|
|
3341
|
+
const lines = history.map((msg, index) => {
|
|
2525
3342
|
const num = String(index + 1).padStart(3, "0");
|
|
2526
3343
|
return `${num}. ${this.summarizeServerMessage(msg)}`;
|
|
2527
3344
|
});
|
|
@@ -2576,7 +3393,10 @@ export class BridgeWebSocketServer {
|
|
|
2576
3393
|
return text ? `assistant: ${text}` : "assistant";
|
|
2577
3394
|
}
|
|
2578
3395
|
case "tool_result": {
|
|
2579
|
-
const contentPreview = msg.content
|
|
3396
|
+
const contentPreview = msg.content
|
|
3397
|
+
.replace(/\s+/g, " ")
|
|
3398
|
+
.trim()
|
|
3399
|
+
.slice(0, 100);
|
|
2580
3400
|
return `${msg.toolName ?? "tool_result"}(${msg.toolUseId}) ${contentPreview}`;
|
|
2581
3401
|
}
|
|
2582
3402
|
case "permission_request":
|