clay-server 2.47.0-beta.5 → 3.0.0-beta.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/lib/daemon.js +6 -27
- package/lib/project-connection.js +21 -8
- package/lib/project-http.js +5 -2
- package/lib/project-session-pair.js +312 -0
- package/lib/project-session-spawn.js +8 -4
- package/lib/project-sessions.js +108 -10
- package/lib/project-user-message.js +18 -8
- package/lib/project.js +33 -0
- package/lib/public/app.js +24 -1
- package/lib/public/css/icon-strip.css +0 -190
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -0
- package/lib/public/css/mobile-nav.css +4 -0
- package/lib/public/css/pane.css +420 -0
- package/lib/public/css/sidebar.css +40 -0
- package/lib/public/css/sticky-notes.css +2 -0
- package/lib/public/css/title-bar.css +169 -0
- package/lib/public/index.html +10 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +6 -10
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-home-hub.js +2 -0
- package/lib/public/modules/app-messages.js +63 -13
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-projects.js +43 -30
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/branch-switcher.js +178 -0
- package/lib/public/modules/dom-refs.js +0 -1
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- package/lib/public/modules/project-switcher.js +2 -1
- package/lib/public/modules/sidebar-mobile.js +19 -6
- package/lib/public/modules/sidebar-projects.js +63 -236
- package/lib/public/modules/sidebar-sessions.js +175 -1
- package/lib/public/modules/sidebar.js +2 -0
- package/lib/public/modules/split-group-helpers.js +18 -0
- package/lib/public/modules/split-pair-ui.js +324 -0
- package/lib/public/modules/split-view.js +499 -0
- package/lib/public/modules/sticky-notes.js +32 -91
- package/lib/public/modules/worktree-family.js +60 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +111 -20
- package/lib/sdk-message-processor.js +2 -2
- package/lib/server.js +24 -4
- package/lib/session-hygiene.js +100 -0
- package/lib/session-pair-mcp-server.js +28 -0
- package/lib/session-split-groups.js +298 -0
- package/lib/sessions.js +163 -34
- package/lib/ws-request.js +14 -0
- package/lib/ws-schema.js +13 -0
- package/lib/yoke/adapters/claude.js +32 -0
- package/lib/yoke/adapters/codex.js +100 -77
- package/lib/yoke/adapters/kiro.js +34 -35
- package/lib/yoke/codex-app-server.js +56 -6
- package/lib/yoke/index.js +1 -0
- package/lib/yoke/skill-discovery.js +196 -0
- package/lib/yoke/vendor-registry.js +34 -0
- package/package.json +1 -1
package/lib/project-sessions.js
CHANGED
|
@@ -175,6 +175,11 @@ function attachSessions(ctx) {
|
|
|
175
175
|
var termId = (typeof s.runtimeTerminalId === "number")
|
|
176
176
|
? s.runtimeTerminalId
|
|
177
177
|
: (typeof s.terminalId === "number" ? s.terminalId : null);
|
|
178
|
+
// Adopted external CLI sessions (a standalone `claude` run in this
|
|
179
|
+
// cwd) must not banner: the user is already looking at that terminal.
|
|
180
|
+
// Once they open the session in Clay a PTY attaches and banners
|
|
181
|
+
// become meaningful.
|
|
182
|
+
if (s.adopted && termId === null) return;
|
|
178
183
|
// Don't banner a session someone is already watching — they can see
|
|
179
184
|
// the reply in the TUI. Gate at the source (server) rather than relying
|
|
180
185
|
// on the client's activeSessionId suppression, which isn't reliable for
|
|
@@ -428,6 +433,9 @@ function attachSessions(ctx) {
|
|
|
428
433
|
}
|
|
429
434
|
|
|
430
435
|
if (msg.type === "new_session") {
|
|
436
|
+
// Opportunistic hygiene: drop blanks that sat untouched past the grace
|
|
437
|
+
// period before deciding whether to reuse or create.
|
|
438
|
+
sm.sweepBlankSessions();
|
|
431
439
|
var sessionOpts = {};
|
|
432
440
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
433
441
|
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
@@ -500,7 +508,27 @@ function attachSessions(ctx) {
|
|
|
500
508
|
startTitleWatcher(newSess);
|
|
501
509
|
sm.switchSession(newSess.localId, ws);
|
|
502
510
|
} else {
|
|
503
|
-
|
|
511
|
+
var newSessionVendor = sessionOpts.vendor || sm.defaultVendor || "claude";
|
|
512
|
+
sessionOpts.effort = yoke.clampEffort(
|
|
513
|
+
newSessionVendor,
|
|
514
|
+
(sm.currentEffortByVendor && sm.currentEffortByVendor[newSessionVendor]) || sm.currentEffort || "medium"
|
|
515
|
+
) || null;
|
|
516
|
+
// Reuse an existing blank GUI session instead of stacking another
|
|
517
|
+
// one. A vendor-less blank is acceptable: nothing has happened in it,
|
|
518
|
+
// so stamping the requested vendor is equivalent to a fresh create.
|
|
519
|
+
var reusable = sm.findReusableBlankSession({
|
|
520
|
+
vendor: sessionOpts.vendor || null,
|
|
521
|
+
ownerId: sessionOpts.ownerId || null,
|
|
522
|
+
});
|
|
523
|
+
if (reusable) {
|
|
524
|
+
if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
|
|
525
|
+
if (!reusable.effort) reusable.effort = sessionOpts.effort;
|
|
526
|
+
if (msg.sessionVisibility) reusable.sessionVisibility = msg.sessionVisibility;
|
|
527
|
+
sm.switchSession(reusable.localId, ws);
|
|
528
|
+
newSess = reusable;
|
|
529
|
+
} else {
|
|
530
|
+
newSess = sm.createSession(sessionOpts, ws);
|
|
531
|
+
}
|
|
504
532
|
}
|
|
505
533
|
ws._clayActiveSession = newSess.localId;
|
|
506
534
|
// Remember the vendor only when the client asked for one explicitly.
|
|
@@ -524,8 +552,8 @@ function attachSessions(ctx) {
|
|
|
524
552
|
}
|
|
525
553
|
}
|
|
526
554
|
var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
527
|
-
userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
528
|
-
if (usersModule.isMultiUser()) {
|
|
555
|
+
if (!ws._clayPane) userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
556
|
+
if (usersModule.isMultiUser() && !ws._clayPane) {
|
|
529
557
|
broadcastPresence();
|
|
530
558
|
}
|
|
531
559
|
return true;
|
|
@@ -642,9 +670,20 @@ function attachSessions(ctx) {
|
|
|
642
670
|
}
|
|
643
671
|
// If the target session's vendor doesn't own the currently cached
|
|
644
672
|
// model, clear sm.currentModel so the UI and next query don't leak
|
|
645
|
-
// the previous session's vendor-specific model into this one.
|
|
673
|
+
// the previous session's vendor-specific model into this one. A
|
|
674
|
+
// session-specific model always wins and survives daemon restarts.
|
|
646
675
|
var switchTargetSess = sm.sessions.get(msg.id);
|
|
647
|
-
|
|
676
|
+
var switchTargetVendor = switchTargetSess && (switchTargetSess.vendor || sm.defaultVendor || "claude");
|
|
677
|
+
if (switchTargetSess && !switchTargetSess.effort) {
|
|
678
|
+
switchTargetSess.effort = yoke.clampEffort(
|
|
679
|
+
switchTargetVendor,
|
|
680
|
+
(sm.currentEffortByVendor && sm.currentEffortByVendor[switchTargetVendor]) || sm.currentEffort || "medium"
|
|
681
|
+
) || null;
|
|
682
|
+
sm.saveSessionFile(switchTargetSess);
|
|
683
|
+
}
|
|
684
|
+
if (switchTargetSess && switchTargetSess.model) {
|
|
685
|
+
sm.currentModel = switchTargetSess.model;
|
|
686
|
+
} else if (switchTargetSess && sm.currentModel) {
|
|
648
687
|
var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
|
|
649
688
|
var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
|
|
650
689
|
var found = false;
|
|
@@ -664,7 +703,7 @@ function attachSessions(ctx) {
|
|
|
664
703
|
if (!usersModule.canAccessSession(ws._clayUser.id, switchTarget, { visibility: "public" })) return true;
|
|
665
704
|
ws._clayActiveSession = msg.id;
|
|
666
705
|
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
667
|
-
broadcastPresence();
|
|
706
|
+
if (!ws._clayPane) broadcastPresence();
|
|
668
707
|
} else {
|
|
669
708
|
ws._clayActiveSession = msg.id;
|
|
670
709
|
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
@@ -675,7 +714,7 @@ function attachSessions(ctx) {
|
|
|
675
714
|
sendTo(ws, { type: "context_sources_state", active: switchedSources });
|
|
676
715
|
}
|
|
677
716
|
var swPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
678
|
-
userPresence.setPresence(slug, swPresKey, msg.id, null);
|
|
717
|
+
if (!ws._clayPane) userPresence.setPresence(slug, swPresKey, msg.id, null);
|
|
679
718
|
}
|
|
680
719
|
return true;
|
|
681
720
|
}
|
|
@@ -799,6 +838,7 @@ function attachSessions(ctx) {
|
|
|
799
838
|
s.title = String(msg.title).substring(0, 100);
|
|
800
839
|
s.titleManuallySet = true;
|
|
801
840
|
sm.saveSessionFile(s);
|
|
841
|
+
sm.notifySessionRenamed(s.localId);
|
|
802
842
|
sm.broadcastSessionList();
|
|
803
843
|
// Sync title to SDK session
|
|
804
844
|
if (s.cliSessionId) {
|
|
@@ -1017,12 +1057,63 @@ function attachSessions(ctx) {
|
|
|
1017
1057
|
sm.currentPermissionMode = msg.mode;
|
|
1018
1058
|
var session = getSessionForWs(ws);
|
|
1019
1059
|
if (session) {
|
|
1060
|
+
session.permissionMode = msg.mode;
|
|
1061
|
+
sm.saveSessionFile(session);
|
|
1062
|
+
sm.broadcastSessionList();
|
|
1020
1063
|
sdk.setPermissionMode(session, msg.mode);
|
|
1021
1064
|
}
|
|
1022
1065
|
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1023
1066
|
return true;
|
|
1024
1067
|
}
|
|
1025
1068
|
|
|
1069
|
+
if (msg.type === "set_session_full_access") {
|
|
1070
|
+
var fullAccessSession = msg.id ? sm.sessions.get(Number(msg.id)) : getSessionForWs(ws);
|
|
1071
|
+
var fullAccessEnabled = msg.enabled === true;
|
|
1072
|
+
var fullAccessMode = fullAccessSession && (fullAccessSession.runtimeMode || fullAccessSession.mode || "gui");
|
|
1073
|
+
if (!fullAccessSession || fullAccessMode !== "gui") {
|
|
1074
|
+
sendTo(ws, { type: "session_full_access_result", ok: false, id: msg.id || null, error: "Skip Permissions is only available for GUI sessions." });
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
if (usersModule.isMultiUser() && ws._clayUser &&
|
|
1078
|
+
!usersModule.canAccessSession(ws._clayUser.id, fullAccessSession, { visibility: "public" })) {
|
|
1079
|
+
sendTo(ws, { type: "session_full_access_result", ok: false, id: fullAccessSession.localId, error: "You do not have access to this session." });
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
var previousFullAccess = fullAccessSession.permissionMode === "bypassPermissions";
|
|
1084
|
+
var previousPermissionMode = fullAccessSession.permissionMode || null;
|
|
1085
|
+
var previousRestoreMode = fullAccessSession.permissionModeBeforeFullAccess || null;
|
|
1086
|
+
if (fullAccessEnabled) {
|
|
1087
|
+
if (!previousFullAccess) {
|
|
1088
|
+
fullAccessSession.permissionModeBeforeFullAccess = fullAccessSession.permissionMode || sm.currentPermissionMode || "default";
|
|
1089
|
+
}
|
|
1090
|
+
fullAccessSession.permissionMode = "bypassPermissions";
|
|
1091
|
+
} else {
|
|
1092
|
+
fullAccessSession.permissionMode = fullAccessSession.permissionModeBeforeFullAccess || "default";
|
|
1093
|
+
fullAccessSession.permissionModeBeforeFullAccess = null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
(async function () {
|
|
1097
|
+
try {
|
|
1098
|
+
// Clay's common canUseTool callback owns permission skipping for
|
|
1099
|
+
// every vendor. If an adapter supports native mode switching, keep
|
|
1100
|
+
// it in a normal mode so Clay remains the single approval layer.
|
|
1101
|
+
if (fullAccessSession.queryInstance && typeof fullAccessSession.queryInstance.setPermissionMode === "function") {
|
|
1102
|
+
await fullAccessSession.queryInstance.setPermissionMode(fullAccessEnabled ? "default" : fullAccessSession.permissionMode);
|
|
1103
|
+
}
|
|
1104
|
+
sm.saveSessionFile(fullAccessSession);
|
|
1105
|
+
sm.broadcastSessionList();
|
|
1106
|
+
send({ type: "session_full_access_changed", id: fullAccessSession.localId, enabled: fullAccessEnabled, permissionMode: fullAccessSession.permissionMode });
|
|
1107
|
+
sendTo(ws, { type: "session_full_access_result", ok: true, id: fullAccessSession.localId, enabled: fullAccessEnabled });
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
fullAccessSession.permissionMode = previousPermissionMode;
|
|
1110
|
+
fullAccessSession.permissionModeBeforeFullAccess = previousRestoreMode;
|
|
1111
|
+
sendTo(ws, { type: "session_full_access_result", ok: false, id: fullAccessSession.localId, error: "Failed to change Skip Permissions: " + (e.message || e) });
|
|
1112
|
+
}
|
|
1113
|
+
})();
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1026
1117
|
if (msg.type === "set_server_default_mode" && msg.mode) {
|
|
1027
1118
|
if (typeof opts.onSetServerDefaultMode === "function") {
|
|
1028
1119
|
opts.onSetServerDefaultMode(msg.mode);
|
|
@@ -1050,12 +1141,19 @@ function attachSessions(ctx) {
|
|
|
1050
1141
|
}
|
|
1051
1142
|
|
|
1052
1143
|
if (msg.type === "set_effort" && msg.effort) {
|
|
1053
|
-
sm.currentEffort = msg.effort;
|
|
1054
1144
|
var session = getSessionForWs(ws);
|
|
1055
1145
|
if (session) {
|
|
1056
|
-
|
|
1146
|
+
var effortVendor = session.vendor || sm.defaultVendor || "claude";
|
|
1147
|
+
var sessionEffort = yoke.clampEffort(effortVendor, msg.effort);
|
|
1148
|
+
if (!sessionEffort) {
|
|
1149
|
+
sendTo(ws, { type: "error", text: "This effort level is not supported by the selected vendor." });
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
sdk.setEffort(session, sessionEffort).catch(function (e) {
|
|
1153
|
+
sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
|
|
1154
|
+
});
|
|
1155
|
+
sm.sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1057
1156
|
}
|
|
1058
|
-
send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1059
1157
|
return true;
|
|
1060
1158
|
}
|
|
1061
1159
|
|
|
@@ -476,22 +476,32 @@ function attachUserMessage(ctx) {
|
|
|
476
476
|
return !!_browserTabList[tid];
|
|
477
477
|
});
|
|
478
478
|
|
|
479
|
+
function startFreshQuery(finalText) {
|
|
480
|
+
session._queryStartTs = Date.now();
|
|
481
|
+
console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
|
|
482
|
+
sdk.startQuery(session, finalText, msg.images, ensureProjectAccessForSession(session));
|
|
483
|
+
}
|
|
484
|
+
|
|
479
485
|
function dispatchToSdk(finalText) {
|
|
480
486
|
if (!session.isProcessing) {
|
|
481
487
|
session.isProcessing = true;
|
|
482
488
|
onProcessingChanged();
|
|
483
489
|
session.sentToolResults = {};
|
|
484
490
|
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
485
|
-
if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
|
|
491
|
+
if (!session.queryInstance && !session._queryStarting && (!session.worker || session.messageQueue !== "worker")) {
|
|
486
492
|
// No active query (or worker idle between queries): start a new query
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
startFreshQuery(finalText);
|
|
494
|
+
} else if (!sdk.pushMessage(session, finalText, msg.images)) {
|
|
495
|
+
// Stale state won the branch (previous stream died between the
|
|
496
|
+
// turn boundary and its cleanup) but there is nothing to deliver
|
|
497
|
+
// to. Start a fresh query with this text instead of dropping it,
|
|
498
|
+
// which used to leave the processing indicator bouncing with no
|
|
499
|
+
// response until the user sent the message again.
|
|
500
|
+
startFreshQuery(finalText);
|
|
492
501
|
}
|
|
493
|
-
} else {
|
|
494
|
-
|
|
502
|
+
} else if (!sdk.pushMessage(session, finalText, msg.images)) {
|
|
503
|
+
// isProcessing was stuck true with no live query: same recovery.
|
|
504
|
+
startFreshQuery(finalText);
|
|
495
505
|
}
|
|
496
506
|
sm.broadcastSessionList();
|
|
497
507
|
}
|
package/lib/project.js
CHANGED
|
@@ -31,6 +31,8 @@ var { attachMcp } = require("./project-mcp");
|
|
|
31
31
|
var { createLocalMcp } = require("./mcp-local");
|
|
32
32
|
var { attachEmail: attachEmailModule } = require("./project-email");
|
|
33
33
|
var { attachSessionSpawn } = require("./project-session-spawn");
|
|
34
|
+
var { attachSessionPair } = require("./project-session-pair");
|
|
35
|
+
var { attachSplitGroups } = require("./session-split-groups");
|
|
34
36
|
// project-notifications is attached globally in server.js, passed via opts.notificationsModule
|
|
35
37
|
|
|
36
38
|
// --- Context Sources persistence ---
|
|
@@ -417,6 +419,13 @@ function createProjectContext(opts) {
|
|
|
417
419
|
sm.availableVendors = Object.keys(adapters);
|
|
418
420
|
sm.defaultVendor = defaultVendor;
|
|
419
421
|
|
|
422
|
+
var _splitGroups = attachSplitGroups({
|
|
423
|
+
sm: sm,
|
|
424
|
+
clients: clients,
|
|
425
|
+
sendTo: sendTo,
|
|
426
|
+
usersModule: usersModule,
|
|
427
|
+
});
|
|
428
|
+
|
|
420
429
|
var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
|
|
421
430
|
var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
|
|
422
431
|
sm._savedDefaultMode = (_projMode && _projMode.mode) || (_srvMode && _srvMode.mode) || "default";
|
|
@@ -479,6 +488,25 @@ function createProjectContext(opts) {
|
|
|
479
488
|
// The SDK bridge is created after local MCP servers. Session spawning uses
|
|
480
489
|
// a getter so tool handlers see the initialized bridge when they run.
|
|
481
490
|
var sdk = null;
|
|
491
|
+
var _sessionPair = attachSessionPair({
|
|
492
|
+
sm: sm,
|
|
493
|
+
isMate: isMate,
|
|
494
|
+
splitStore: _splitGroups.store,
|
|
495
|
+
getSdk: function () { return sdk; },
|
|
496
|
+
send: send,
|
|
497
|
+
sendTo: sendTo,
|
|
498
|
+
broadcastDelegation: function (group, message) {
|
|
499
|
+
for (var pairWs of clients) {
|
|
500
|
+
if (pairWs.readyState !== 1) continue;
|
|
501
|
+
var visibleGroups = _splitGroups.store.listFor(pairWs);
|
|
502
|
+
var canSee = visibleGroups.some(function (visibleGroup) { return visibleGroup.id === group.id; });
|
|
503
|
+
if (canSee) sendTo(pairWs, message);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
usersModule: usersModule,
|
|
507
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
508
|
+
onProcessingChanged: onProcessingChanged,
|
|
509
|
+
});
|
|
482
510
|
var _sessionSpawn = attachSessionSpawn({
|
|
483
511
|
cwd: cwd,
|
|
484
512
|
sm: sm,
|
|
@@ -488,6 +516,7 @@ function createProjectContext(opts) {
|
|
|
488
516
|
usersModule: usersModule,
|
|
489
517
|
adapters: adapters,
|
|
490
518
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
519
|
+
getPairToolDefs: function (boundSession) { return _sessionPair.getToolDefs(boundSession); },
|
|
491
520
|
});
|
|
492
521
|
|
|
493
522
|
// --- MCP tool servers (created via YOKE adapter) ---
|
|
@@ -734,6 +763,7 @@ function createProjectContext(opts) {
|
|
|
734
763
|
}
|
|
735
764
|
return false;
|
|
736
765
|
},
|
|
766
|
+
getSessionSystemPrompt: function (session) { return _sessionPair.getSystemPrompt(session); },
|
|
737
767
|
});
|
|
738
768
|
|
|
739
769
|
// --- Loop engine (delegated to project-loop.js) ---
|
|
@@ -1056,6 +1086,8 @@ function createProjectContext(opts) {
|
|
|
1056
1086
|
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
1057
1087
|
|
|
1058
1088
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1089
|
+
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1090
|
+
if (_splitGroups.handleMessage(ws, msg)) return;
|
|
1059
1091
|
if (_sessions.handleSessionsMessage(ws, msg)) return;
|
|
1060
1092
|
|
|
1061
1093
|
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
@@ -1465,6 +1497,7 @@ function createProjectContext(opts) {
|
|
|
1465
1497
|
_loop: _loop,
|
|
1466
1498
|
_mcp: _mcp,
|
|
1467
1499
|
_notifications: _notifications,
|
|
1500
|
+
_splitGroups: _splitGroups,
|
|
1468
1501
|
resolveSessionForView: _sessions.resolveSessionForView,
|
|
1469
1502
|
hydrateImageRefs: hydrateImageRefs,
|
|
1470
1503
|
broadcastClientCount: broadcastClientCount,
|
package/lib/public/app.js
CHANGED
|
@@ -42,6 +42,9 @@ import { initProfile, getProfileLang } from './modules/profile.js';
|
|
|
42
42
|
import { initUserSettings } from './modules/user-settings.js';
|
|
43
43
|
import { initToolPalettes } from './modules/tool-palette.js';
|
|
44
44
|
import { initProjectSwitcher } from './modules/project-switcher.js';
|
|
45
|
+
import { initBranchSwitcher } from './modules/branch-switcher.js';
|
|
46
|
+
import { initSplitView } from './modules/split-view.js';
|
|
47
|
+
import { initPaneBridge } from './modules/pane-bridge.js';
|
|
45
48
|
import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
46
49
|
import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
|
|
47
50
|
import { initTooltips, registerTooltip } from './modules/tooltip.js';
|
|
@@ -74,9 +77,18 @@ import { initMention, handleMentionStart, handleMentionStream, handleMentionDone
|
|
|
74
77
|
import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf, renderMcpDebateProposal } from './modules/debate.js';
|
|
75
78
|
|
|
76
79
|
// --- Base path for multi-project routing ---
|
|
80
|
+
var bootParams = new URLSearchParams(location.search);
|
|
81
|
+
var paneMode = bootParams.get("pane") === "1";
|
|
82
|
+
var paneSessionId = Number(bootParams.get("session"));
|
|
83
|
+
if (!Number.isSafeInteger(paneSessionId) || paneSessionId < 1) paneSessionId = null;
|
|
84
|
+
if (paneMode) document.body.classList.add("pane-mode");
|
|
77
85
|
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
78
86
|
var basePath = slugMatch ? "/p/" + slugMatch[1] + "/" : "/";
|
|
79
87
|
var wsPath = slugMatch ? "/p/" + slugMatch[1] + "/ws" : "/ws";
|
|
88
|
+
if (paneMode) {
|
|
89
|
+
wsPath += "?pane=1";
|
|
90
|
+
if (paneSessionId) wsPath += "&session=" + paneSessionId;
|
|
91
|
+
}
|
|
80
92
|
|
|
81
93
|
// --- DOM refs ---
|
|
82
94
|
var $ = function (id) { return document.getElementById(id); };
|
|
@@ -85,7 +97,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
85
97
|
var sendBtn = $("send-btn");
|
|
86
98
|
function getStatusDot() {
|
|
87
99
|
return document.querySelector("#icon-strip-projects .icon-strip-item.active .icon-strip-status") ||
|
|
88
|
-
document.querySelector("#icon-strip-projects .icon-strip-wt-item.active .icon-strip-status") ||
|
|
89
100
|
document.querySelector("#icon-strip-users .icon-strip-mate.active .icon-strip-status");
|
|
90
101
|
}
|
|
91
102
|
var headerTitleEl = $("header-title");
|
|
@@ -262,6 +273,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
262
273
|
// session / routing
|
|
263
274
|
basePath: basePath,
|
|
264
275
|
wsPath: wsPath,
|
|
276
|
+
paneMode: paneMode,
|
|
277
|
+
paneSessionId: paneSessionId,
|
|
278
|
+
panePinPending: paneMode && !!paneSessionId,
|
|
279
|
+
splitPanes: null,
|
|
280
|
+
splitGroups: [],
|
|
281
|
+
splitDelegations: {},
|
|
265
282
|
dmMode: false,
|
|
266
283
|
historyFrom: 0,
|
|
267
284
|
loadingMore: false,
|
|
@@ -270,6 +287,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
270
287
|
projectName: projectName,
|
|
271
288
|
cwd: "",
|
|
272
289
|
currentSlug: currentSlug,
|
|
290
|
+
projects: [],
|
|
291
|
+
homeHubVisible: false,
|
|
273
292
|
currentProjectOwnerId: null,
|
|
274
293
|
isOsUsers: false,
|
|
275
294
|
skipPermsEnabled: false,
|
|
@@ -322,6 +341,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
322
341
|
currentBetas: [],
|
|
323
342
|
currentThinking: "adaptive",
|
|
324
343
|
currentThinkingBudget: 10000,
|
|
344
|
+
sessionFullAccess: false,
|
|
325
345
|
codexApproval: null,
|
|
326
346
|
codexSandbox: null,
|
|
327
347
|
codexWebSearch: null,
|
|
@@ -461,6 +481,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
461
481
|
// keydown listener registers later — capture-phase ordering doesn't
|
|
462
482
|
// matter here but it keeps related bootstrap steps adjacent.
|
|
463
483
|
initProjectSwitcher();
|
|
484
|
+
initBranchSwitcher();
|
|
485
|
+
initSplitView();
|
|
486
|
+
initPaneBridge();
|
|
464
487
|
|
|
465
488
|
// --- Connect overlay (animated ASCII logo) ---
|
|
466
489
|
var asciiLogoCanvas = $("ascii-logo-canvas");
|
|
@@ -223,7 +223,6 @@
|
|
|
223
223
|
|
|
224
224
|
/* Show dot only on active item or when processing */
|
|
225
225
|
.icon-strip-item.active .icon-strip-status,
|
|
226
|
-
.icon-strip-wt-item.active .icon-strip-status,
|
|
227
226
|
.icon-strip-status.processing {
|
|
228
227
|
opacity: 1;
|
|
229
228
|
}
|
|
@@ -248,7 +247,6 @@
|
|
|
248
247
|
|
|
249
248
|
/* Pending permission shake */
|
|
250
249
|
.icon-strip-item.has-pending-perm,
|
|
251
|
-
.icon-strip-wt-item.has-pending-perm,
|
|
252
250
|
.icon-strip-mate.has-pending-perm {
|
|
253
251
|
animation: permShake 2s ease-in-out infinite;
|
|
254
252
|
}
|
|
@@ -1146,155 +1144,6 @@
|
|
|
1146
1144
|
opacity: 1;
|
|
1147
1145
|
}
|
|
1148
1146
|
|
|
1149
|
-
/* --- Worktree folder groups --- */
|
|
1150
|
-
.icon-strip-group {
|
|
1151
|
-
display: flex;
|
|
1152
|
-
flex-direction: column;
|
|
1153
|
-
align-items: center;
|
|
1154
|
-
position: relative;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.icon-strip-group .folder-header {
|
|
1158
|
-
position: relative;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
.icon-strip-group-chevron {
|
|
1162
|
-
position: absolute;
|
|
1163
|
-
bottom: 2px;
|
|
1164
|
-
right: 2px;
|
|
1165
|
-
width: 16px;
|
|
1166
|
-
height: 16px;
|
|
1167
|
-
border-radius: 50%;
|
|
1168
|
-
background: var(--bg-alt);
|
|
1169
|
-
border: 1px solid var(--border);
|
|
1170
|
-
display: flex;
|
|
1171
|
-
align-items: center;
|
|
1172
|
-
justify-content: center;
|
|
1173
|
-
color: var(--text-dimmer);
|
|
1174
|
-
pointer-events: auto;
|
|
1175
|
-
cursor: pointer;
|
|
1176
|
-
transition: transform 0.15s ease, background 0.15s, border-color 0.15s;
|
|
1177
|
-
z-index: 2;
|
|
1178
|
-
line-height: 1;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
.icon-strip-group-chevron i,
|
|
1182
|
-
.icon-strip-group-chevron svg {
|
|
1183
|
-
width: 10px;
|
|
1184
|
-
height: 10px;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
.icon-strip-group-chevron:hover {
|
|
1188
|
-
background: var(--bg-hover, var(--bg-alt));
|
|
1189
|
-
border-color: var(--text-dimmer);
|
|
1190
|
-
color: var(--text-secondary);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
.icon-strip-group.collapsed .icon-strip-group-chevron {
|
|
1194
|
-
background: var(--border);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
.icon-strip-group-items {
|
|
1198
|
-
display: flex;
|
|
1199
|
-
flex-direction: column;
|
|
1200
|
-
align-items: center;
|
|
1201
|
-
overflow: hidden;
|
|
1202
|
-
transition: max-height 0.2s ease;
|
|
1203
|
-
max-height: 500px;
|
|
1204
|
-
border-left: 2px solid var(--border);
|
|
1205
|
-
margin-left: 0;
|
|
1206
|
-
padding-left: 0;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
.icon-strip-group.collapsed .icon-strip-group-items {
|
|
1210
|
-
max-height: 0;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
/* Worktree items (smaller than regular project icons) */
|
|
1214
|
-
.icon-strip-wt-item {
|
|
1215
|
-
width: 48px;
|
|
1216
|
-
height: 40px;
|
|
1217
|
-
display: flex;
|
|
1218
|
-
align-items: center;
|
|
1219
|
-
justify-content: center;
|
|
1220
|
-
cursor: pointer;
|
|
1221
|
-
position: relative;
|
|
1222
|
-
text-decoration: none;
|
|
1223
|
-
color: var(--text-secondary);
|
|
1224
|
-
transition: background 0.15s;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
.icon-strip-wt-item::before {
|
|
1228
|
-
content: "";
|
|
1229
|
-
position: absolute;
|
|
1230
|
-
width: 30px;
|
|
1231
|
-
height: 30px;
|
|
1232
|
-
border-radius: 8px;
|
|
1233
|
-
background: var(--bg-alt);
|
|
1234
|
-
opacity: 0.7;
|
|
1235
|
-
transition: opacity 0.15s, background 0.15s;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.icon-strip-wt-item:hover::before {
|
|
1239
|
-
opacity: 1;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
.icon-strip-wt-item.active::before {
|
|
1243
|
-
background: var(--accent);
|
|
1244
|
-
opacity: 1;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
.icon-strip-wt-item.active {
|
|
1248
|
-
color: #fff;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
.icon-strip-wt-item .wt-branch-abbrev {
|
|
1252
|
-
font-size: 11px;
|
|
1253
|
-
font-weight: 600;
|
|
1254
|
-
letter-spacing: -0.5px;
|
|
1255
|
-
pointer-events: none;
|
|
1256
|
-
position: relative;
|
|
1257
|
-
z-index: 1;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/* Disabled/inaccessible worktree */
|
|
1261
|
-
.icon-strip-wt-item.wt-disabled {
|
|
1262
|
-
opacity: 0.35;
|
|
1263
|
-
cursor: not-allowed;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
.icon-strip-wt-item.wt-disabled::after {
|
|
1267
|
-
content: "\1F6AB";
|
|
1268
|
-
position: absolute;
|
|
1269
|
-
font-size: 14px;
|
|
1270
|
-
z-index: 3;
|
|
1271
|
-
filter: grayscale(0.3);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
/* Group add button */
|
|
1275
|
-
.icon-strip-group-add {
|
|
1276
|
-
width: 30px;
|
|
1277
|
-
height: 30px;
|
|
1278
|
-
border-radius: 8px;
|
|
1279
|
-
border: 1.5px dashed var(--border);
|
|
1280
|
-
background: transparent;
|
|
1281
|
-
color: var(--text-dimmer);
|
|
1282
|
-
font-size: 16px;
|
|
1283
|
-
font-weight: 400;
|
|
1284
|
-
cursor: pointer;
|
|
1285
|
-
display: flex;
|
|
1286
|
-
align-items: center;
|
|
1287
|
-
justify-content: center;
|
|
1288
|
-
margin: 2px 0 4px 0;
|
|
1289
|
-
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
.icon-strip-group-add:hover {
|
|
1293
|
-
border-color: var(--accent);
|
|
1294
|
-
color: var(--accent);
|
|
1295
|
-
background: rgba(var(--accent-rgb, 124, 58, 237), 0.08);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
1147
|
/* --- Worktree creation modal --- */
|
|
1299
1148
|
.wt-modal-overlay {
|
|
1300
1149
|
position: fixed;
|
|
@@ -1409,45 +1258,6 @@ select.wt-modal-input {
|
|
|
1409
1258
|
cursor: not-allowed;
|
|
1410
1259
|
}
|
|
1411
1260
|
|
|
1412
|
-
/* --- Mobile worktree folder --- */
|
|
1413
|
-
.mobile-project-folder {
|
|
1414
|
-
display: flex;
|
|
1415
|
-
flex-direction: column;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
.mobile-project-item.wt-item {
|
|
1419
|
-
padding-left: 28px;
|
|
1420
|
-
opacity: 0.85;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
.mobile-project-item.wt-disabled {
|
|
1424
|
-
opacity: 0.35;
|
|
1425
|
-
cursor: not-allowed;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
.mobile-folder-chevron {
|
|
1429
|
-
font-size: 8px;
|
|
1430
|
-
color: var(--text-dimmer);
|
|
1431
|
-
margin-left: auto;
|
|
1432
|
-
cursor: pointer;
|
|
1433
|
-
transition: transform 0.15s ease;
|
|
1434
|
-
padding: 4px;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
.mobile-project-folder.collapsed .mobile-folder-chevron {
|
|
1438
|
-
transform: rotate(-90deg);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
.mobile-folder-items {
|
|
1442
|
-
overflow: hidden;
|
|
1443
|
-
transition: max-height 0.2s ease;
|
|
1444
|
-
max-height: 500px;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
.mobile-project-folder.collapsed .mobile-folder-items {
|
|
1448
|
-
max-height: 0;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
1261
|
/* --- Per-project presence avatars --- */
|
|
1452
1262
|
/* --- Mobile: hide icon strip --- */
|
|
1453
1263
|
/* --- Skeleton loading placeholders --- */
|