clay-server 4.2.0 → 4.3.0-beta.10

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 (84) hide show
  1. package/lib/driver-continuation-access.js +54 -0
  2. package/lib/driver-continuation-lease.js +52 -0
  3. package/lib/driver-continuation-pair-transfer.js +86 -0
  4. package/lib/driver-continuation-pair.js +86 -0
  5. package/lib/driver-continuation-record.js +40 -0
  6. package/lib/driver-continuation-startup.js +68 -0
  7. package/lib/driver-continuation-transaction.js +61 -0
  8. package/lib/driver-continuation-trigger.js +110 -0
  9. package/lib/multi-worker-feature.js +11 -0
  10. package/lib/project-connection.js +3 -1
  11. package/lib/project-driver-continuation.js +499 -0
  12. package/lib/project-message-delivery.js +20 -0
  13. package/lib/project-pair-autonomous-stop.js +14 -8
  14. package/lib/project-pair-close-transaction.js +59 -0
  15. package/lib/project-pair-global-stop.js +18 -0
  16. package/lib/project-pair-lifecycle-status.js +34 -0
  17. package/lib/project-pair-lifecycle.js +117 -135
  18. package/lib/project-pair-message.js +1 -1
  19. package/lib/project-pair-owned-stop.js +7 -4
  20. package/lib/project-pair-replacement-state.js +5 -1
  21. package/lib/project-pair-result-capture.js +116 -0
  22. package/lib/project-pair-result-delivery.js +140 -0
  23. package/lib/project-pair-result-outbox.js +400 -0
  24. package/lib/project-pair-result-pipeline.js +16 -0
  25. package/lib/project-pair-result-recovery.js +229 -0
  26. package/lib/project-pair-structural-close.js +20 -0
  27. package/lib/project-pair-task-control.js +45 -18
  28. package/lib/project-pair-worker-close.js +68 -0
  29. package/lib/project-session-handoff.js +48 -55
  30. package/lib/project-session-pair-prompt.js +26 -0
  31. package/lib/project-session-pair.js +87 -106
  32. package/lib/project-sessions.js +4 -0
  33. package/lib/project-user-message.js +21 -0
  34. package/lib/project-worker-creation-proposal.js +176 -0
  35. package/lib/project-worker-permission.js +7 -8
  36. package/lib/project-worker-proposal-runner.js +45 -0
  37. package/lib/project-worker-proposal.js +79 -115
  38. package/lib/project.js +87 -3
  39. package/lib/public/app.js +1 -0
  40. package/lib/public/css/driver-continuation.css +56 -0
  41. package/lib/public/css/messages.css +12 -0
  42. package/lib/public/css/mobile-nav.css +29 -14
  43. package/lib/public/css/overlays.css +54 -25
  44. package/lib/public/css/pane.css +66 -12
  45. package/lib/public/css/sidebar.css +62 -7
  46. package/lib/public/index.html +3 -3
  47. package/lib/public/modules/app-connection.js +165 -74
  48. package/lib/public/modules/app-messages.js +31 -3
  49. package/lib/public/modules/driver-continuation-state.js +76 -0
  50. package/lib/public/modules/driver-continuation.js +260 -0
  51. package/lib/public/modules/message-delivery-ui.js +63 -0
  52. package/lib/public/modules/message-delivery.js +163 -6
  53. package/lib/public/modules/pair-result-status.js +87 -0
  54. package/lib/public/modules/permission-control.js +10 -5
  55. package/lib/public/modules/project-activation.js +10 -7
  56. package/lib/public/modules/session-creation-cta.js +46 -0
  57. package/lib/public/modules/sidebar-mobile.js +42 -30
  58. package/lib/public/modules/sidebar-sessions.js +77 -52
  59. package/lib/public/modules/split-group-helpers.js +74 -2
  60. package/lib/public/modules/split-pair-ui.js +28 -6
  61. package/lib/public/modules/split-pane-reconciler.js +28 -0
  62. package/lib/public/modules/split-pane-renderer.js +107 -0
  63. package/lib/public/modules/split-view.js +58 -188
  64. package/lib/public/modules/websocket-lifecycle.js +178 -0
  65. package/lib/public/modules/websocket-watchdog.js +126 -0
  66. package/lib/public/modules/worker-pane-lock.js +13 -3
  67. package/lib/public/style.css +1 -0
  68. package/lib/sdk-bridge.js +77 -0
  69. package/lib/server.js +1 -0
  70. package/lib/session-handoff-discovery.js +443 -0
  71. package/lib/session-handoff-mcp-server.js +32 -4
  72. package/lib/session-pair-factory.js +66 -0
  73. package/lib/session-pair-mcp-server.js +10 -5
  74. package/lib/session-pair-prompts.js +46 -25
  75. package/lib/session-pair-target.js +42 -0
  76. package/lib/session-pair-turn-control.js +30 -9
  77. package/lib/session-split-group-anchors.js +126 -0
  78. package/lib/session-split-group-persistence.js +136 -0
  79. package/lib/session-split-group-roles.js +164 -0
  80. package/lib/session-split-group-v2-store.js +146 -0
  81. package/lib/session-split-groups.js +293 -133
  82. package/lib/sessions.js +89 -22
  83. package/lib/ws-schema.js +8 -0
  84. package/package.json +1 -1
package/lib/project.js CHANGED
@@ -55,7 +55,11 @@ var { attachDelegatedFollowUp } = require("./project-delegated-follow-up");
55
55
  var { attachCapsuleTurn } = require("./project-capsule-turn");
56
56
  var workspaceSessionRef = require("./workspace-query-service").sessionRef;
57
57
  var { attachSessionPair } = require("./project-session-pair");
58
+ var { attachPairResultOutbox } = require("./project-pair-result-outbox");
59
+ var { attachPairResultRecovery } = require("./project-pair-result-recovery");
58
60
  var { attachSessionHandoff } = require("./project-session-handoff");
61
+ var { attachDriverContinuation } = require("./project-driver-continuation");
62
+ var driverContinuationAccess = require("./driver-continuation-access");
59
63
  var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
60
64
  var { attachSessionDocument } = require("./project-session-document");
61
65
  var { attachCapsuleCatalog } = require("./project-capsule-catalog");
@@ -64,6 +68,7 @@ var { attachProjectLogs } = require("./project-logs");
64
68
  var { attachProjectLogFeedbackDelivery } = require("./project-log-feedback-delivery");
65
69
  var { attachProjectMateKnowledge } = require("./project-mate-knowledge");
66
70
  var { attachSplitGroups } = require("./session-split-groups");
71
+ var multiWorkerFeature = require("./multi-worker-feature");
67
72
  var toolControlMcp = require("./tool-control-mcp-server");
68
73
  var capsuleFloor = require("./capsule-display-floor");
69
74
  var toolLlm = require("./tool-llm");
@@ -203,6 +208,7 @@ function safeAbsPath(requested) {
203
208
  * opts: { cwd, slug, title, pushModule, debug, dangerouslySkipPermissions, currentVersion }
204
209
  */
205
210
  function createProjectContext(opts) {
211
+ var multiWorkerGate = multiWorkerFeature.fromServerConfig(opts);
206
212
  var cwd = opts.cwd;
207
213
  var slug = opts.slug;
208
214
  var project = path.basename(cwd);
@@ -657,6 +663,7 @@ function createProjectContext(opts) {
657
663
  clients: clients,
658
664
  sendTo: sendTo,
659
665
  usersModule: usersModule,
666
+ multiWorkerFeature: multiWorkerGate,
660
667
  onPairChanged: function (group) {
661
668
  if (!sdk || !group || !Array.isArray(group.members)) return;
662
669
  for (var i = 0; i < group.members.length; i++) {
@@ -817,10 +824,14 @@ function createProjectContext(opts) {
817
824
 
818
825
  // The SDK bridge is created after local MCP servers. Session spawning uses
819
826
  // a getter so tool handlers see the initialized bridge when they run.
827
+ var _pairResultOutbox = attachPairResultOutbox({ storageDir: sm.sessionsDir, projectSlug: slug });
828
+ _pairResultOutbox.recoverPending(sm.sessions);
829
+ var _pairResultRecovery = null;
820
830
  var _sessionPair = attachSessionPair({
821
831
  sm: sm,
822
832
  isMate: isMate,
823
833
  splitStore: _splitGroups.store,
834
+ multiWorkerFeature: multiWorkerGate,
824
835
  getSdk: function () { return sdk; },
825
836
  send: send,
826
837
  sendTo: sendTo,
@@ -835,12 +846,31 @@ function createProjectContext(opts) {
835
846
  usersModule: usersModule,
836
847
  getLinuxUserForSession: getLinuxUserForSession,
837
848
  onProcessingChanged: onProcessingChanged,
849
+ projectSlug: slug,
850
+ resultOutbox: _pairResultOutbox,
851
+ onResultStateChange: function (driver, record) { if (_pairResultRecovery) _pairResultRecovery.stateChanged(driver, record); },
838
852
  adapters: adapters,
839
853
  onPartnerResult: function (driver, worker, token) {
840
854
  if (_scheduledTaskExecution) _scheduledTaskExecution.onPartnerResult(driver, worker, token);
841
855
  if (_autonomousRun) _autonomousRun.onPartnerResult(driver, worker, token);
842
856
  },
843
857
  });
858
+ var _pairWorkerClose = require("./project-pair-worker-close").attachPairWorkerClose({
859
+ sm: sm,
860
+ splitStore: _splitGroups.store,
861
+ multiWorkerFeature: multiWorkerGate,
862
+ isMate: isMate,
863
+ projectSlug: slug,
864
+ hasSocket: function (ws) { return clients.has(ws); },
865
+ sendTo: sendTo,
866
+ closeWorker: _sessionPair.closeWorker,
867
+ });
868
+ _pairResultRecovery = attachPairResultRecovery({
869
+ sm: sm, store: _splitGroups.store, outbox: _pairResultOutbox, resultCapture: _sessionPair.resultCapture,
870
+ wake: _sessionPair.wakePartnerResult, projectSlug: slug, sendTo: sendTo,
871
+ blockedReason: _sessionPair.stopBarrier,
872
+ getClients: function () { return clients; }, isMultiUser: usersModule.isMultiUser,
873
+ });
844
874
  var _sessionSpawn = attachSessionSpawn({
845
875
  cwd: cwd,
846
876
  sm: sm,
@@ -854,16 +884,41 @@ function createProjectContext(opts) {
854
884
  });
855
885
  var _sessionHandoff = attachSessionHandoff({
856
886
  cwd: cwd,
887
+ projectSlug: slug,
857
888
  sm: sm,
858
889
  isMate: isMate,
859
890
  splitStore: _splitGroups.store,
860
891
  getSdk: function () { return sdk; },
861
892
  sendTo: sendTo,
862
893
  usersModule: usersModule,
894
+ isMultiUser: usersModule.isMultiUser,
895
+ getProjectAccess: opts.getProjectAccess || function () { return { visibility: "public", ownerId: projectOwnerId || null }; },
896
+ canAccessSession: usersModule.canAccessSession,
897
+ findUserById: usersModule.findUserById,
898
+ isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
863
899
  adapters: adapters,
864
900
  getLinuxUserForSession: getLinuxUserForSession,
865
901
  onProcessingChanged: onProcessingChanged,
866
902
  });
903
+ var _driverContinuation = attachDriverContinuation({
904
+ sm: sm,
905
+ isMate: isMate,
906
+ projectSlug: slug,
907
+ splitStore: _splitGroups.store,
908
+ pendingMessageQueue: _pendingMessageQueue,
909
+ dangerouslySkipPermissions: dangerouslySkipPermissions,
910
+ isMultiUser: usersModule.isMultiUser,
911
+ isDriverOperatedSession: function (session) { return !isMate && _sessionPair.workerPermission.isDriverOperated(session); },
912
+ authorizeSession: function (session, ownerId) {
913
+ return driverContinuationAccess.authorize({ usersModule: usersModule, projectSlug: slug,
914
+ getProjectAccess: opts.getProjectAccess, canAccessProjectSlug: opts.canAccessProjectSlug }, session, ownerId);
915
+ },
916
+ getSdk: function () { return sdk; },
917
+ getLinuxUserForSession: getLinuxUserForSession,
918
+ onProcessingChanged: onProcessingChanged,
919
+ consumePendingMessage: function (session) { return _userMessage && _userMessage.consumePendingMessage(session); },
920
+ sendTo: sendTo,
921
+ });
867
922
  var _debate = null;
868
923
  var _debateProposal = attachDebateProposal({
869
924
  cwd: cwd,
@@ -1250,6 +1305,7 @@ function createProjectContext(opts) {
1250
1305
  },
1251
1306
  onProcessingChanged: onProcessingChanged,
1252
1307
  onSessionAvailable: function (session) {
1308
+ if (_pairResultRecovery) _pairResultRecovery.available(session);
1253
1309
  if (_scheduledMessages) _scheduledMessages.notify();
1254
1310
  if (_userMessage && session) setImmediate(function () { _userMessage.consumePendingMessage(session); });
1255
1311
  },
@@ -1287,6 +1343,7 @@ function createProjectContext(opts) {
1287
1343
  _sessionPair.getSystemPrompt(session),
1288
1344
  _sessionNotes.getSystemPrompt(session),
1289
1345
  _sessionHandoff.getSystemPrompt(session),
1346
+ _driverContinuation.getSystemPrompt(session),
1290
1347
  _sessionDocument.getSystemPrompt(session),
1291
1348
  _capsuleCatalog.getSystemPrompt(session),
1292
1349
  _projectLogs.getSystemPrompt(session),
@@ -1304,6 +1361,7 @@ function createProjectContext(opts) {
1304
1361
  return _sessionPair.getToolDefs(session)
1305
1362
  .concat(_sessionNotes.getToolDefs(session))
1306
1363
  .concat(_sessionHandoff.getToolDefs(session))
1364
+ .concat(_driverContinuation.getToolDefs(session, Number(session._sdkQueryGeneration || 0)))
1307
1365
  .concat(_sessionDocument.getToolDefs(session))
1308
1366
  .concat(_debateProposal.getToolDefs(session))
1309
1367
  .concat(_mateCreationProposal.getToolDefs(session))
@@ -1712,8 +1770,11 @@ function createProjectContext(opts) {
1712
1770
  if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
1713
1771
 
1714
1772
  // --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
1773
+ if (_pairResultRecovery.handleMessage(ws, msg)) return;
1774
+ if (_pairWorkerClose.handleMessage(ws, msg)) return;
1715
1775
  if (_sessionPair.handleMessage(ws, msg)) return;
1716
1776
  if (_sessionHandoff.handleMessage(ws, msg)) return;
1777
+ if (_driverContinuation.handleMessage(ws, msg)) return;
1717
1778
  if (_splitGroups.handleMessage(ws, msg)) return;
1718
1779
  if (_models.handleMessage(ws, msg)) return;
1719
1780
  if (_scheduledTasks.handleMessage(ws, msg)) return;
@@ -1986,6 +2047,7 @@ function createProjectContext(opts) {
1986
2047
  serverAuthToken: serverAuthToken,
1987
2048
  resolveDefaultAi: opts.resolveDefaultAi,
1988
2049
  });
2050
+ _pairResultRecovery.reconcile();
1989
2051
  var _toolLlmRuntime = attachProjectToolLlmRuntime({
1990
2052
  users: usersModule,
1991
2053
  osUsers: osUsers,
@@ -2036,7 +2098,11 @@ function createProjectContext(opts) {
2036
2098
  if (isMate || !_sessionPair.workerPermission) return false;
2037
2099
  return _sessionPair.workerPermission.isDriverOperated(session);
2038
2100
  },
2039
- beginHumanPairTurn: function (session) { return _sessionPair.beginHumanTurn(session); },
2101
+ beginHumanPairTurn: function (session) {
2102
+ var resumed = _sessionPair.beginHumanTurn(session);
2103
+ if (resumed) _pairResultRecovery.beginHumanTurn(session);
2104
+ return resumed;
2105
+ },
2040
2106
  authorizePendingDispatch: function (session, actor, item) {
2041
2107
  if (!session || sm.sessions.get(session.localId) !== session || !item || item.projectSlug !== slug) return false;
2042
2108
  if (_scheduledMessages && !_scheduledMessages.authorizePending(item, session)) return false;
@@ -2226,7 +2292,7 @@ function createProjectContext(opts) {
2226
2292
  inputSchema: normalizeToolSchema(noteTools[nti].inputSchema),
2227
2293
  });
2228
2294
  }
2229
- var handoffTools = _sessionHandoff.getToolDefs(boundSession);
2295
+ var handoffTools = _sessionHandoff.getToolDefs(boundSession, queryGeneration);
2230
2296
  for (var hti = 0; hti < handoffTools.length; hti++) {
2231
2297
  tools.push({
2232
2298
  server: "clay-handoff",
@@ -2235,6 +2301,15 @@ function createProjectContext(opts) {
2235
2301
  inputSchema: normalizeToolSchema(handoffTools[hti].inputSchema),
2236
2302
  });
2237
2303
  }
2304
+ var continuationTools = _driverContinuation.getToolDefs(boundSession, queryGeneration);
2305
+ for (var cti = 0; cti < continuationTools.length; cti++) {
2306
+ tools.push({
2307
+ server: "clay-continuation",
2308
+ name: continuationTools[cti].name,
2309
+ description: continuationTools[cti].description || continuationTools[cti].name,
2310
+ inputSchema: normalizeToolSchema(continuationTools[cti].inputSchema),
2311
+ });
2312
+ }
2238
2313
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2239
2314
  for (var dti = 0; dti < documentTools.length; dti++) {
2240
2315
  tools.push({
@@ -2321,13 +2396,21 @@ function createProjectContext(opts) {
2321
2396
  }
2322
2397
  }
2323
2398
  if (boundSession && serverName === "clay-handoff") {
2324
- var handoffTools = _sessionHandoff.getToolDefs(boundSession);
2399
+ var handoffTools = _sessionHandoff.getToolDefs(boundSession, queryGeneration);
2325
2400
  for (var hti = 0; hti < handoffTools.length; hti++) {
2326
2401
  if (handoffTools[hti].name === toolName && typeof handoffTools[hti].handler === "function") {
2327
2402
  return Promise.resolve(handoffTools[hti].handler(args || {}));
2328
2403
  }
2329
2404
  }
2330
2405
  }
2406
+ if (boundSession && serverName === "clay-continuation") {
2407
+ var continuationTools = _driverContinuation.getToolDefs(boundSession, queryGeneration);
2408
+ for (var cti = 0; cti < continuationTools.length; cti++) {
2409
+ if (continuationTools[cti].name === toolName && typeof continuationTools[cti].handler === "function") {
2410
+ return Promise.resolve(continuationTools[cti].handler(args || {}));
2411
+ }
2412
+ }
2413
+ }
2331
2414
  if (boundSession && serverName === "clay-documents") {
2332
2415
  var documentTools = _sessionDocument.getToolDefs(boundSession);
2333
2416
  for (var dti = 0; dti < documentTools.length; dti++) {
@@ -2452,6 +2535,7 @@ function createProjectContext(opts) {
2452
2535
  _email: _email,
2453
2536
  restoreDebateState: restoreDebateState,
2454
2537
  scheduledMessages: _scheduledMessages,
2538
+ pairResultRecovery: _pairResultRecovery,
2455
2539
  sendCursorSharingState: opts.sendCursorSharingState,
2456
2540
  stopFileWatch: stopFileWatch,
2457
2541
  stopAllDirWatches: stopAllDirWatches,
package/lib/public/app.js CHANGED
@@ -276,6 +276,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
276
276
  splitPanes: null,
277
277
  splitGroups: [],
278
278
  splitDelegations: {},
279
+ pairResultStatus: [],
279
280
  notesTemporarilyHidden: false,
280
281
  dmMode: false,
281
282
  historyFrom: 0,
@@ -0,0 +1,56 @@
1
+ .driver-continuation-card,
2
+ .driver-continuation-context {
3
+ width: min(680px, calc(100% - 24px));
4
+ margin: 14px auto;
5
+ border: 1px solid var(--border);
6
+ border-radius: 14px;
7
+ background: var(--bg-alt);
8
+ color: var(--text);
9
+ box-shadow: 0 10px 28px rgba(0, 0, 0, .08);
10
+ }
11
+
12
+ .driver-continuation-card { padding: 18px; }
13
+ .driver-continuation-card header,
14
+ .driver-continuation-context { display: flex; align-items: flex-start; gap: 12px; }
15
+ .driver-continuation-context { padding: 15px 17px; }
16
+ .driver-continuation-card header > div,
17
+ .driver-continuation-context-copy { display: grid; gap: 3px; min-width: 0; flex: 1; }
18
+ .driver-continuation-mark { display: grid; place-items: center; width: 30px; height: 30px; flex: 0 0 auto; border-radius: 9px; color: var(--accent2); background: color-mix(in srgb, var(--accent2) 12%, transparent); }
19
+ .driver-continuation-kicker { font-size: 10px; line-height: 1.2; font-weight: 700; letter-spacing: .1em; color: var(--text-muted); }
20
+ .driver-continuation-status { margin-left: auto; white-space: nowrap; font-size: 12px; color: var(--text-muted); }
21
+ .driver-continuation-reason,
22
+ .driver-continuation-context p { margin: 12px 0; color: var(--text-secondary); line-height: 1.5; }
23
+ .driver-continuation-summary { display: grid; grid-template-columns: 80px 1fr; gap: 7px 12px; padding: 13px; border-radius: 10px; background: var(--bg); }
24
+ .driver-continuation-summary dt,
25
+ .driver-continuation-details dt { font-size: 11px; font-weight: 700; color: var(--text-muted); }
26
+ .driver-continuation-summary dd,
27
+ .driver-continuation-details dd { margin: 0; white-space: pre-wrap; line-height: 1.45; }
28
+ .driver-continuation-details { margin-top: 12px; color: var(--text-secondary); }
29
+ .driver-continuation-details summary { cursor: pointer; font-size: 12px; font-weight: 600; }
30
+ .driver-continuation-details dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 12px; margin: 12px 0 0; }
31
+ .driver-continuation-error { padding: 10px; border-radius: 8px; color: var(--error); background: var(--error-8); }
32
+ .driver-continuation-context .driver-continuation-error { color: var(--error); }
33
+ .driver-continuation-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
34
+ .driver-continuation-action,
35
+ .driver-continuation-source { min-height: 36px; border: 1px solid var(--border); border-radius: 9px; padding: 8px 13px; font: inherit; cursor: pointer; }
36
+ .driver-continuation-action.primary { display: inline-flex; align-items: center; gap: 7px; border-color: var(--accent2); background: color-mix(in srgb, var(--accent2) 16%, var(--bg)); color: var(--accent2); }
37
+ .driver-continuation-action.secondary,
38
+ .driver-continuation-source { background: transparent; color: var(--text); }
39
+ .driver-continuation-action:disabled,
40
+ .driver-continuation-source:disabled { cursor: not-allowed; opacity: .45; }
41
+ .driver-continuation-action:focus-visible,
42
+ .driver-continuation-source:focus-visible,
43
+ .driver-continuation-details summary:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; }
44
+ .driver-continuation-source { justify-self: start; margin-top: 5px; padding: 5px 9px; min-height: 30px; font-size: 12px; }
45
+
46
+ @media (max-width: 600px) {
47
+ .driver-continuation-card,
48
+ .driver-continuation-context { width: calc(100% - 16px); }
49
+ .driver-continuation-card { padding: 14px; }
50
+ .driver-continuation-card header { flex-wrap: wrap; }
51
+ .driver-continuation-status { width: 100%; margin-left: 42px; }
52
+ .driver-continuation-summary,
53
+ .driver-continuation-details dl { grid-template-columns: 1fr; }
54
+ .driver-continuation-actions { flex-direction: column-reverse; }
55
+ .driver-continuation-action { width: 100%; justify-content: center; }
56
+ }
@@ -1387,6 +1387,18 @@ pre.mermaid-error {
1387
1387
  .plan-banner.plan-exit { color: var(--success); }
1388
1388
  .plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
1389
1389
 
1390
+ .pair-result-status { display: grid; gap: 8px; padding: 10px 16px 0; }
1391
+ .pair-result-status-card { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-alt); padding: 10px 12px; color: var(--text-secondary); font-size: 12px; }
1392
+ .pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
1393
+ .pair-result-status-card > strong { color: var(--text); margin-bottom: 3px; }
1394
+ .pair-result-status-card details { margin-top: 7px; }
1395
+ .pair-result-status-card summary { cursor: pointer; color: var(--accent); }
1396
+ .pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; font: inherit; }
1397
+ .pair-result-status-card button { margin-top: 8px; }
1398
+ .pair-result-status-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1399
+ .pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
1400
+ .pair-result-status-uncertain, .pair-result-status-blocked { border-color: color-mix(in srgb, var(--warning) 45%, var(--border)); }
1401
+
1390
1402
  /* --- Plan card --- */
1391
1403
  .plan-card {
1392
1404
  max-width: var(--content-width);
@@ -358,11 +358,15 @@
358
358
  background: rgba(var(--overlay-rgb), 0.06);
359
359
  }
360
360
 
361
- /* Split "New session" row: main action on the last-used vendor + a chevron
362
- that expands .mobile-vendor-list below it. Mirrors the desktop sidebar. */
361
+ .mobile-session-new:focus-visible {
362
+ outline: 2px solid var(--accent);
363
+ outline-offset: 2px;
364
+ }
365
+
366
+ /* Primary creation action with an adjacent icon-only provider selector. */
363
367
  .mobile-session-new-row {
364
368
  display: flex;
365
- gap: 6px;
369
+ gap: 0;
366
370
  padding: 6px 8px 8px;
367
371
  }
368
372
 
@@ -370,33 +374,44 @@
370
374
  flex: 1 1 auto;
371
375
  width: auto;
372
376
  margin-bottom: 0;
377
+ min-width: 0;
373
378
  padding: 10px 12px;
374
379
  border: 1px solid var(--border-subtle);
375
380
  border-radius: 8px;
376
381
  background: rgba(var(--overlay-rgb), 0.03);
377
382
  color: var(--text);
378
383
  gap: 8px;
379
- }
380
- .mobile-session-new-row .mobile-session-new-chevron {
381
- flex: 0 0 auto;
382
- width: 44px;
384
+ border-radius: 8px 0 0 8px;
385
+ white-space: normal;
386
+ line-height: 1.2;
387
+ }
388
+ .mobile-session-new-row .mobile-session-new-provider {
389
+ flex: 0 0 34px;
390
+ width: 34px;
391
+ min-width: 34px;
383
392
  margin-bottom: 0;
384
- padding: 10px 0;
393
+ padding: 0;
385
394
  border: 1px solid var(--border-subtle);
386
- border-radius: 8px;
395
+ border-left-color: var(--border);
396
+ border-radius: 0 8px 8px 0;
387
397
  background: rgba(var(--overlay-rgb), 0.03);
388
398
  color: var(--text-muted);
389
- justify-content: center;
399
+ display: inline-flex;
400
+ align-items: center;
390
401
  gap: 0;
391
- }
392
- .mobile-session-new-row .mobile-session-new-chevron.expanded svg {
393
- transform: rotate(180deg);
402
+ justify-content: center;
394
403
  }
395
404
  .mobile-session-new-row .mobile-session-new-main:active,
396
- .mobile-session-new-row .mobile-session-new-chevron:active {
405
+ .mobile-session-new-row .mobile-session-new-provider:active {
397
406
  background: rgba(var(--overlay-rgb), 0.1);
398
407
  }
399
408
 
409
+ .mobile-session-new-row .session-create-provider-chevron .lucide,
410
+ .mobile-session-new-row .session-create-provider-chevron svg {
411
+ width: 14px;
412
+ height: 14px;
413
+ }
414
+
400
415
  .mobile-vendor-list {
401
416
  display: flex;
402
417
  flex-direction: column;
@@ -518,13 +518,13 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
518
518
  .popover-copy:hover { color: var(--text); border-color: var(--text-dimmer); }
519
519
  .popover-copy.copied { color: var(--success); border-color: var(--success); }
520
520
 
521
- /* Skip permissions pill */
521
+ /* Quiet native toolbar control for session permission mode. */
522
522
  .session-permission-control {
523
523
  display: inline-flex;
524
524
  align-items: center;
525
525
  flex: 0 1 auto;
526
- flex-wrap: wrap;
527
- gap: 7px;
526
+ flex-wrap: nowrap;
527
+ gap: 5px;
528
528
  max-width: 100%;
529
529
  min-width: 0;
530
530
  }
@@ -533,52 +533,81 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
533
533
  display: inline-flex;
534
534
  align-items: center;
535
535
  flex: 0 0 auto;
536
- gap: 2px;
536
+ gap: 1px;
537
537
  padding: 2px;
538
- border: 1px solid var(--border, rgba(255,255,255,.18));
539
- border-radius: 999px;
540
- background: var(--surface-raised, rgba(0,0,0,.18));
538
+ border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
539
+ border-radius: 8px;
540
+ background: color-mix(in srgb, var(--bg-alt) 72%, transparent);
541
541
  }
542
542
  .session-permission-segmented button {
543
- flex: 0 0 auto;
543
+ position: relative;
544
+ flex: 0 0 48px;
545
+ width: 48px;
546
+ height: 22px;
544
547
  border: 0;
545
- border-radius: 999px;
546
- padding: 3px 8px;
547
- color: var(--text-muted, inherit);
548
+ border-radius: 7px;
549
+ padding: 3px 4px;
550
+ color: var(--text-secondary);
548
551
  background: transparent;
549
552
  font: inherit;
553
+ font-weight: 500;
550
554
  font-size: 11px;
555
+ line-height: 1;
551
556
  white-space: nowrap;
552
557
  cursor: pointer;
553
- }
554
- .session-permission-segmented button.active { background: var(--accent, #6f8cff); color: #fff; }
555
- .session-permission-segmented button:disabled { cursor: not-allowed; opacity: .5; }
558
+ transition: color .15s ease, background .15s ease, box-shadow .15s ease, border-color .15s ease;
559
+ }
560
+ .session-permission-segmented button:hover:not(:disabled) { color: var(--text); background: color-mix(in srgb, var(--bg) 76%, transparent); }
561
+ .session-permission-segmented button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 1; }
562
+ .session-permission-segmented button.active { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); font-weight: var(--font-weight-heading, 600); box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .12); }
563
+ :root.light-theme .session-permission-segmented button.active { background: var(--bg); }
564
+ .session-permission-segmented button:disabled { cursor: not-allowed; }
565
+ .session-permission-control:not(.permission-pending) .session-permission-segmented button:disabled { color: var(--text-muted); opacity: .65; }
556
566
  .session-permission-control.permission-pending .session-permission-segmented button:disabled { cursor: wait; }
567
+ .session-permission-segmented button.pending { color: var(--text); background: color-mix(in srgb, var(--bg) 42%, transparent); }
568
+ .session-permission-segmented button.pending .permission-label { visibility: hidden; }
569
+ .permission-spinner { position: absolute; inset: 50% auto auto 50%; display: block; visibility: hidden; width: 10px; height: 10px; transform: translate(-50%, -50%); border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; }
570
+ .session-permission-segmented button.pending .permission-spinner { visibility: visible; animation: permission-spin .7s linear infinite; }
571
+ @keyframes permission-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
557
572
  .session-permission-status {
558
- flex: 1 1 118px;
573
+ flex: 0 1 auto;
559
574
  min-width: 0;
560
- max-width: 190px;
561
- overflow-wrap: anywhere;
562
- white-space: normal;
575
+ max-width: 180px;
576
+ overflow: hidden;
577
+ text-overflow: ellipsis;
578
+ white-space: nowrap;
579
+ padding: 3px 6px;
580
+ border: 1px solid transparent;
581
+ border-radius: 6px;
563
582
  color: var(--text-dimmer, var(--text-muted));
564
583
  font-size: 10px;
565
584
  line-height: 1.2;
566
585
  }
567
586
  .session-permission-status.hidden { display: none; }
587
+ .session-permission-control.permission-pending .session-permission-status:not(.hidden) {
588
+ position: absolute;
589
+ width: 1px;
590
+ height: 1px;
591
+ padding: 0;
592
+ margin: -1px;
593
+ overflow: hidden;
594
+ clip: rect(0, 0, 0, 0);
595
+ white-space: nowrap;
596
+ border: 0;
597
+ }
568
598
  .permission-runtime-fallback .session-permission-status { color: var(--warning, #d49a55); }
569
599
  .permission-globally-forced .session-permission-status { color: var(--warning, #d49a55); }
570
- .permission-label-short { display: none; }
571
600
  @media (max-width: 760px) {
572
- .permission-label-long { display: none; }
573
- .permission-label-short { display: inline; }
574
- .session-permission-control { column-gap: 5px; row-gap: 2px; }
575
- .session-permission-segmented button { padding-inline: 7px; }
601
+ .session-permission-control { column-gap: 4px; }
576
602
  .session-permission-status {
577
- flex-basis: 96px;
578
- max-width: 170px;
603
+ max-width: 132px;
579
604
  font-size: 9px;
580
605
  }
581
606
  }
607
+ @media (prefers-reduced-motion: reduce) {
608
+ .session-permission-segmented button { transition: none; }
609
+ .session-permission-segmented button.pending .permission-spinner { animation: none; opacity: .7; }
610
+ }
582
611
  #skip-perms-pill .lucide { width: 12px; height: 12px; flex-shrink: 0; }
583
612
 
584
613
  /* --- Confirm modal --- */
@@ -73,6 +73,28 @@ body.pane-mode #input-area {
73
73
  display: flex;
74
74
  }
75
75
 
76
+ /* A configured two-Worker group is a deliberate workspace hierarchy: the
77
+ Driver gets the broad working canvas, while both Workers remain visible as
78
+ equal-height execution surfaces on the right. Legacy/ad-hoc two-pane
79
+ splits keep the ordinary equal columns below. */
80
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) {
81
+ display: grid;
82
+ grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr);
83
+ grid-template-rows: repeat(2, minmax(0, 1fr));
84
+ gap: 1px;
85
+ background: var(--border-subtle);
86
+ }
87
+
88
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-driver {
89
+ grid-row: 1 / span 2;
90
+ grid-column: 1;
91
+ }
92
+
93
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
94
+ grid-column: 2;
95
+ grid-row: var(--split-worker-row, auto);
96
+ }
97
+
76
98
  .split-pane {
77
99
  flex: 1;
78
100
  display: flex;
@@ -365,26 +387,34 @@ body.pane-mode #input-area {
365
387
  line-height: 1;
366
388
  }
367
389
 
368
- /* Header-scoped (two-class) selectors: pane.css loads BEFORE menus.css in
369
- style.css, so the base .session-full-access rules would win a specificity
370
- tie and keep the pill at its 22px title-bar size inside the 24px pane
371
- header. Scope under .split-pane-header to outrank them. */
390
+ /* Header-scoped selectors keep the compact permission control clear of the
391
+ title and close affordance while preserving the shared header rhythm. */
392
+ .split-pane-header {
393
+ height: 28px;
394
+ min-height: 28px;
395
+ }
396
+
372
397
  .split-pane-header .split-pane-full-access {
373
- height: auto;
398
+ height: 24px;
374
399
  align-self: center;
375
400
  margin-left: 0;
376
401
  padding: 0;
377
402
  gap: 4px;
378
- font-size: 8px;
403
+ font-size: 11px;
379
404
  letter-spacing: 0.02em;
380
405
  flex-wrap: nowrap;
381
406
  flex: 0 0 auto;
382
- overflow: hidden;
383
407
  }
384
408
 
385
409
  .split-pane-header .session-permission-segmented button {
386
- padding: 1px 5px;
387
- font-size: 8px;
410
+ min-height: 20px;
411
+ height: 20px;
412
+ font-size: 11px;
413
+ }
414
+
415
+ .split-pane-header .session-permission-segmented {
416
+ height: 24px;
417
+ padding: 1px;
388
418
  }
389
419
 
390
420
  .split-pane-header .session-permission-status {
@@ -394,7 +424,7 @@ body.pane-mode #input-area {
394
424
  overflow: hidden;
395
425
  text-overflow: ellipsis;
396
426
  white-space: nowrap;
397
- font-size: 8px;
427
+ font-size: 10px;
398
428
  }
399
429
 
400
430
  .split-pane-header .session-full-access-track {
@@ -433,6 +463,11 @@ body.pane-mode #input-area {
433
463
  color: var(--text);
434
464
  }
435
465
 
466
+ .split-pane-close:disabled {
467
+ opacity: 0.42;
468
+ cursor: not-allowed;
469
+ }
470
+
436
471
  .split-pane-close svg {
437
472
  width: 12px;
438
473
  height: 12px;
@@ -470,6 +505,25 @@ body.pane-mode #input-area {
470
505
  --split-worker-height: calc(30% - 4.8px);
471
506
  }
472
507
 
508
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) {
509
+ display: flex;
510
+ flex-direction: column;
511
+ gap: var(--split-pane-gap);
512
+ background: transparent;
513
+ }
514
+
515
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-driver,
516
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
517
+ grid-row: auto;
518
+ grid-column: auto;
519
+ }
520
+
521
+ #split-host:has(.split-pane-role-worker:nth-of-type(3)) .split-pane-role-worker {
522
+ flex: 0 1 24%;
523
+ min-height: 104px;
524
+ order: -1;
525
+ }
526
+
473
527
  #split-host.visible {
474
528
  flex-direction: column;
475
529
  gap: var(--split-pane-gap);
@@ -487,11 +541,11 @@ body.pane-mode #input-area {
487
541
  }
488
542
 
489
543
  .split-pane-role-worker {
490
- order: -1;
544
+ order: var(--split-pane-order, -1);
491
545
  }
492
546
 
493
547
  .split-pane-role-driver {
494
- order: 0;
548
+ order: var(--split-pane-order, 0);
495
549
  }
496
550
 
497
551
  /* A configured Worker is an execution summary; the Driver keeps the