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.
Files changed (46) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-session-pair.js +312 -0
  4. package/lib/project-session-spawn.js +8 -4
  5. package/lib/project-sessions.js +108 -10
  6. package/lib/project-user-message.js +18 -8
  7. package/lib/project.js +33 -0
  8. package/lib/public/app.js +20 -0
  9. package/lib/public/css/menus.css +83 -1
  10. package/lib/public/css/messages.css +14 -0
  11. package/lib/public/css/pane.css +420 -0
  12. package/lib/public/css/sidebar.css +40 -0
  13. package/lib/public/css/sticky-notes.css +2 -0
  14. package/lib/public/index.html +4 -0
  15. package/lib/public/modules/app-connection.js +6 -0
  16. package/lib/public/modules/app-favicon.js +5 -0
  17. package/lib/public/modules/app-header.js +101 -6
  18. package/lib/public/modules/app-messages.js +59 -6
  19. package/lib/public/modules/app-panels.js +49 -27
  20. package/lib/public/modules/app-rendering.js +11 -4
  21. package/lib/public/modules/pane-bridge.js +41 -0
  22. package/lib/public/modules/pane-session.js +18 -0
  23. package/lib/public/modules/sidebar-sessions.js +175 -1
  24. package/lib/public/modules/sidebar.js +2 -0
  25. package/lib/public/modules/split-group-helpers.js +18 -0
  26. package/lib/public/modules/split-pair-ui.js +324 -0
  27. package/lib/public/modules/split-view.js +499 -0
  28. package/lib/public/modules/sticky-notes.js +32 -91
  29. package/lib/public/style.css +1 -0
  30. package/lib/sdk-bridge.js +111 -20
  31. package/lib/sdk-message-processor.js +2 -2
  32. package/lib/server.js +24 -4
  33. package/lib/session-hygiene.js +100 -0
  34. package/lib/session-pair-mcp-server.js +28 -0
  35. package/lib/session-split-groups.js +298 -0
  36. package/lib/sessions.js +163 -34
  37. package/lib/ws-request.js +14 -0
  38. package/lib/ws-schema.js +13 -0
  39. package/lib/yoke/adapters/claude.js +32 -0
  40. package/lib/yoke/adapters/codex.js +100 -77
  41. package/lib/yoke/adapters/kiro.js +34 -35
  42. package/lib/yoke/codex-app-server.js +56 -6
  43. package/lib/yoke/index.js +1 -0
  44. package/lib/yoke/skill-discovery.js +196 -0
  45. package/lib/yoke/vendor-registry.js +34 -0
  46. package/package.json +2 -2
@@ -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
- newSess = sm.createSession(sessionOpts, ws);
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
- if (switchTargetSess && sm.currentModel) {
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
- sdk.setEffort(session, msg.effort);
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
- session._queryStartTs = Date.now();
488
- console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
489
- sdk.startQuery(session, finalText, msg.images, ensureProjectAccessForSession(session));
490
- } else {
491
- sdk.pushMessage(session, finalText, msg.images);
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
- sdk.pushMessage(session, finalText, msg.images);
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");
@@ -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;