clay-server 2.47.0-beta.6 → 3.0.0-beta.2
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-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 +20 -0
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -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/index.html +4 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +5 -0
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-messages.js +59 -6
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- 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/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 +2 -2
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
|
@@ -43,6 +43,8 @@ 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
45
|
import { initBranchSwitcher } from './modules/branch-switcher.js';
|
|
46
|
+
import { initSplitView } from './modules/split-view.js';
|
|
47
|
+
import { initPaneBridge } from './modules/pane-bridge.js';
|
|
46
48
|
import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
47
49
|
import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
|
|
48
50
|
import { initTooltips, registerTooltip } from './modules/tooltip.js';
|
|
@@ -75,9 +77,18 @@ import { initMention, handleMentionStart, handleMentionStream, handleMentionDone
|
|
|
75
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';
|
|
76
78
|
|
|
77
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");
|
|
78
85
|
var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
|
|
79
86
|
var basePath = slugMatch ? "/p/" + slugMatch[1] + "/" : "/";
|
|
80
87
|
var wsPath = slugMatch ? "/p/" + slugMatch[1] + "/ws" : "/ws";
|
|
88
|
+
if (paneMode) {
|
|
89
|
+
wsPath += "?pane=1";
|
|
90
|
+
if (paneSessionId) wsPath += "&session=" + paneSessionId;
|
|
91
|
+
}
|
|
81
92
|
|
|
82
93
|
// --- DOM refs ---
|
|
83
94
|
var $ = function (id) { return document.getElementById(id); };
|
|
@@ -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,
|
|
@@ -324,6 +341,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
324
341
|
currentBetas: [],
|
|
325
342
|
currentThinking: "adaptive",
|
|
326
343
|
currentThinkingBudget: 10000,
|
|
344
|
+
sessionFullAccess: false,
|
|
327
345
|
codexApproval: null,
|
|
328
346
|
codexSandbox: null,
|
|
329
347
|
codexWebSearch: null,
|
|
@@ -464,6 +482,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
464
482
|
// matter here but it keeps related bootstrap steps adjacent.
|
|
465
483
|
initProjectSwitcher();
|
|
466
484
|
initBranchSwitcher();
|
|
485
|
+
initSplitView();
|
|
486
|
+
initPaneBridge();
|
|
467
487
|
|
|
468
488
|
// --- Connect overlay (animated ASCII logo) ---
|
|
469
489
|
var asciiLogoCanvas = $("ascii-logo-canvas");
|
package/lib/public/css/menus.css
CHANGED
|
@@ -43,6 +43,27 @@
|
|
|
43
43
|
#header-rename-btn:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.04); border-color: var(--border); }
|
|
44
44
|
#header-rename-btn .lucide { width: 13px; height: 13px; }
|
|
45
45
|
|
|
46
|
+
#header-add-worker-btn {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
width: 24px;
|
|
51
|
+
height: 24px;
|
|
52
|
+
border: 1px solid transparent;
|
|
53
|
+
background: none;
|
|
54
|
+
color: var(--text-dimmer);
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
padding: 0;
|
|
59
|
+
margin-left: 4px;
|
|
60
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
61
|
+
}
|
|
62
|
+
#header-add-worker-btn:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.04); border-color: var(--border); }
|
|
63
|
+
#header-add-worker-btn .lucide { width: 13px; height: 13px; }
|
|
64
|
+
#header-add-worker-btn.hidden { display: none; }
|
|
65
|
+
body.dm-mode #header-add-worker-btn { display: none; }
|
|
66
|
+
|
|
46
67
|
/* "Close terminal" button: sits just left of the rename pencil during a live
|
|
47
68
|
TUI session. Matches the rename button's chrome; reds out on hover since it
|
|
48
69
|
stops the running claude (the session stays resumable). */
|
|
@@ -96,6 +117,52 @@
|
|
|
96
117
|
stroke-width: 2.5;
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
.session-full-access {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 6px;
|
|
124
|
+
height: 22px;
|
|
125
|
+
margin-left: 7px;
|
|
126
|
+
padding: 0 6px 0 8px;
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
background: rgba(var(--overlay-rgb), 0.025);
|
|
130
|
+
color: var(--text-dimmer);
|
|
131
|
+
font: inherit;
|
|
132
|
+
font-size: 10px;
|
|
133
|
+
font-weight: 650;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
137
|
+
}
|
|
138
|
+
.session-full-access:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb), 0.05); }
|
|
139
|
+
.session-full-access.hidden { display: none; }
|
|
140
|
+
.session-full-access.active {
|
|
141
|
+
color: var(--error);
|
|
142
|
+
border-color: color-mix(in srgb, var(--error) 38%, var(--border));
|
|
143
|
+
background: color-mix(in srgb, var(--error) 8%, transparent);
|
|
144
|
+
}
|
|
145
|
+
.session-full-access-track {
|
|
146
|
+
position: relative;
|
|
147
|
+
width: 22px;
|
|
148
|
+
height: 12px;
|
|
149
|
+
border-radius: 999px;
|
|
150
|
+
background: var(--border);
|
|
151
|
+
transition: background 0.15s;
|
|
152
|
+
}
|
|
153
|
+
.session-full-access-track > span {
|
|
154
|
+
position: absolute;
|
|
155
|
+
top: 2px;
|
|
156
|
+
left: 2px;
|
|
157
|
+
width: 8px;
|
|
158
|
+
height: 8px;
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
background: var(--text-dimmer);
|
|
161
|
+
transition: transform 0.15s, background 0.15s;
|
|
162
|
+
}
|
|
163
|
+
.session-full-access.active .session-full-access-track { background: var(--error); }
|
|
164
|
+
.session-full-access.active .session-full-access-track > span { background: var(--bg); transform: translateX(10px); }
|
|
165
|
+
|
|
99
166
|
/* Session info popover */
|
|
100
167
|
.session-info-popover {
|
|
101
168
|
position: fixed;
|
|
@@ -110,6 +177,19 @@
|
|
|
110
177
|
color: var(--text-secondary);
|
|
111
178
|
line-height: 1.6;
|
|
112
179
|
}
|
|
180
|
+
.session-info-popover .info-section-title {
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
color: var(--text);
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
text-overflow: ellipsis;
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
max-width: 320px;
|
|
187
|
+
}
|
|
188
|
+
.session-info-popover .info-section-title:not(:first-child) {
|
|
189
|
+
margin-top: 8px;
|
|
190
|
+
padding-top: 8px;
|
|
191
|
+
border-top: 1px solid var(--border-subtle);
|
|
192
|
+
}
|
|
113
193
|
.session-info-popover .info-row {
|
|
114
194
|
display: flex;
|
|
115
195
|
align-items: center;
|
|
@@ -858,6 +938,9 @@
|
|
|
858
938
|
}
|
|
859
939
|
#context-mini:hover { opacity: 0.8; }
|
|
860
940
|
#context-mini.hidden { display: none; }
|
|
941
|
+
/* Split view: per-pane chips replace the shell-level context gauge. */
|
|
942
|
+
#context-mini.split-hidden { display: none; }
|
|
943
|
+
.header-context.hidden { display: none; }
|
|
861
944
|
.context-mini-bar {
|
|
862
945
|
flex: 1;
|
|
863
946
|
height: 4px;
|
|
@@ -898,4 +981,3 @@
|
|
|
898
981
|
#config-chip-label { display: none; }
|
|
899
982
|
}
|
|
900
983
|
|
|
901
|
-
|
|
@@ -165,6 +165,20 @@
|
|
|
165
165
|
display: flex;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
.msg-user-delegated .bubble {
|
|
169
|
+
background: color-mix(in srgb, var(--accent) 11%, var(--bg-elevated, var(--bg-hover)));
|
|
170
|
+
border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.msg-user-delegated .dm-bubble-avatar-other {
|
|
174
|
+
border-radius: 7px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.msg-user-delegated .dm-bubble-name {
|
|
178
|
+
color: var(--accent);
|
|
179
|
+
font-weight: 650;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
/* --- User message action bar --- */
|
|
169
183
|
.msg-actions {
|
|
170
184
|
display: flex;
|