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.
Files changed (58) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-http.js +5 -2
  4. package/lib/project-session-pair.js +312 -0
  5. package/lib/project-session-spawn.js +8 -4
  6. package/lib/project-sessions.js +108 -10
  7. package/lib/project-user-message.js +18 -8
  8. package/lib/project.js +33 -0
  9. package/lib/public/app.js +24 -1
  10. package/lib/public/css/icon-strip.css +0 -190
  11. package/lib/public/css/menus.css +83 -1
  12. package/lib/public/css/messages.css +14 -0
  13. package/lib/public/css/mobile-nav.css +4 -0
  14. package/lib/public/css/pane.css +420 -0
  15. package/lib/public/css/sidebar.css +40 -0
  16. package/lib/public/css/sticky-notes.css +2 -0
  17. package/lib/public/css/title-bar.css +169 -0
  18. package/lib/public/index.html +10 -0
  19. package/lib/public/modules/app-connection.js +6 -0
  20. package/lib/public/modules/app-favicon.js +6 -10
  21. package/lib/public/modules/app-header.js +101 -6
  22. package/lib/public/modules/app-home-hub.js +2 -0
  23. package/lib/public/modules/app-messages.js +63 -13
  24. package/lib/public/modules/app-panels.js +49 -27
  25. package/lib/public/modules/app-projects.js +43 -30
  26. package/lib/public/modules/app-rendering.js +11 -4
  27. package/lib/public/modules/branch-switcher.js +178 -0
  28. package/lib/public/modules/dom-refs.js +0 -1
  29. package/lib/public/modules/pane-bridge.js +41 -0
  30. package/lib/public/modules/pane-session.js +18 -0
  31. package/lib/public/modules/project-switcher.js +2 -1
  32. package/lib/public/modules/sidebar-mobile.js +19 -6
  33. package/lib/public/modules/sidebar-projects.js +63 -236
  34. package/lib/public/modules/sidebar-sessions.js +175 -1
  35. package/lib/public/modules/sidebar.js +2 -0
  36. package/lib/public/modules/split-group-helpers.js +18 -0
  37. package/lib/public/modules/split-pair-ui.js +324 -0
  38. package/lib/public/modules/split-view.js +499 -0
  39. package/lib/public/modules/sticky-notes.js +32 -91
  40. package/lib/public/modules/worktree-family.js +60 -0
  41. package/lib/public/style.css +1 -0
  42. package/lib/sdk-bridge.js +111 -20
  43. package/lib/sdk-message-processor.js +2 -2
  44. package/lib/server.js +24 -4
  45. package/lib/session-hygiene.js +100 -0
  46. package/lib/session-pair-mcp-server.js +28 -0
  47. package/lib/session-split-groups.js +298 -0
  48. package/lib/sessions.js +163 -34
  49. package/lib/ws-request.js +14 -0
  50. package/lib/ws-schema.js +13 -0
  51. package/lib/yoke/adapters/claude.js +32 -0
  52. package/lib/yoke/adapters/codex.js +100 -77
  53. package/lib/yoke/adapters/kiro.js +34 -35
  54. package/lib/yoke/codex-app-server.js +56 -6
  55. package/lib/yoke/index.js +1 -0
  56. package/lib/yoke/skill-discovery.js +196 -0
  57. package/lib/yoke/vendor-registry.js +34 -0
  58. package/package.json +1 -1
@@ -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
@@ -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 --- */