clay-server 4.2.0 → 4.3.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 (34) 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 +82 -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/project-driver-continuation.js +499 -0
  10. package/lib/project-pair-lifecycle.js +73 -73
  11. package/lib/project-pair-replacement-state.js +5 -1
  12. package/lib/project-session-handoff.js +48 -55
  13. package/lib/project-user-message.js +14 -0
  14. package/lib/project-worker-proposal.js +1 -1
  15. package/lib/project.js +49 -2
  16. package/lib/public/css/driver-continuation.css +56 -0
  17. package/lib/public/css/overlays.css +44 -16
  18. package/lib/public/css/pane.css +9 -5
  19. package/lib/public/index.html +3 -3
  20. package/lib/public/modules/app-messages.js +14 -0
  21. package/lib/public/modules/driver-continuation-state.js +76 -0
  22. package/lib/public/modules/driver-continuation.js +260 -0
  23. package/lib/public/modules/permission-control.js +8 -3
  24. package/lib/public/style.css +1 -0
  25. package/lib/sdk-bridge.js +77 -0
  26. package/lib/session-handoff-discovery.js +443 -0
  27. package/lib/session-handoff-mcp-server.js +32 -4
  28. package/lib/session-pair-prompts.js +5 -0
  29. package/lib/session-pair-turn-control.js +19 -3
  30. package/lib/session-split-group-anchors.js +44 -0
  31. package/lib/session-split-groups.js +209 -59
  32. package/lib/sessions.js +20 -2
  33. package/lib/ws-schema.js +6 -0
  34. package/package.json +1 -1
@@ -523,8 +523,8 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
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,80 @@ 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, #45484f) 88%, transparent);
539
+ border-radius: 10px;
540
+ background: color-mix(in srgb, var(--surface-raised, rgba(0,0,0,.18)) 86%, transparent);
541
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04), 0 2px 8px rgba(0,0,0,.12);
541
542
  }
542
543
  .session-permission-segmented button {
544
+ position: relative;
543
545
  flex: 0 0 auto;
544
546
  border: 0;
545
- border-radius: 999px;
546
- padding: 3px 8px;
547
+ border-radius: 7px;
548
+ padding: 4px 9px;
547
549
  color: var(--text-muted, inherit);
548
550
  background: transparent;
549
551
  font: inherit;
550
552
  font-size: 11px;
553
+ line-height: 1;
551
554
  white-space: nowrap;
552
555
  cursor: pointer;
556
+ transition: color .15s ease, background .15s ease, box-shadow .15s ease;
553
557
  }
554
- .session-permission-segmented button.active { background: var(--accent, #6f8cff); color: #fff; }
558
+ .session-permission-segmented button:hover:not(:disabled) { color: var(--text, #fff); background: rgba(255,255,255,.07); }
559
+ .session-permission-segmented button:focus-visible { outline: 2px solid var(--accent, #6f8cff); outline-offset: 1px; z-index: 1; }
560
+ .session-permission-segmented button.active { background: var(--accent, #6f8cff); color: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.2); }
555
561
  .session-permission-segmented button:disabled { cursor: not-allowed; opacity: .5; }
556
562
  .session-permission-control.permission-pending .session-permission-segmented button:disabled { cursor: wait; }
563
+ .session-permission-segmented button.pending { color: #fff; }
564
+ .permission-spinner { display: inline-block; visibility: hidden; width: 10px; height: 10px; margin-left: 4px; vertical-align: -1px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; }
565
+ .session-permission-segmented button.pending .permission-spinner { visibility: visible; animation: permission-spin .7s linear infinite; }
566
+ @keyframes permission-spin { to { transform: rotate(360deg); } }
557
567
  .session-permission-status {
558
- flex: 1 1 118px;
568
+ flex: 0 1 auto;
559
569
  min-width: 0;
560
- max-width: 190px;
561
- overflow-wrap: anywhere;
562
- white-space: normal;
570
+ max-width: 180px;
571
+ overflow: hidden;
572
+ text-overflow: ellipsis;
573
+ white-space: nowrap;
574
+ padding: 3px 6px;
575
+ border: 1px solid transparent;
576
+ border-radius: 6px;
563
577
  color: var(--text-dimmer, var(--text-muted));
564
578
  font-size: 10px;
565
579
  line-height: 1.2;
566
580
  }
567
581
  .session-permission-status.hidden { display: none; }
582
+ .session-permission-control.permission-pending .session-permission-status:not(.hidden) {
583
+ position: absolute;
584
+ width: 1px;
585
+ height: 1px;
586
+ padding: 0;
587
+ margin: -1px;
588
+ overflow: hidden;
589
+ clip: rect(0, 0, 0, 0);
590
+ white-space: nowrap;
591
+ border: 0;
592
+ }
568
593
  .permission-runtime-fallback .session-permission-status { color: var(--warning, #d49a55); }
569
594
  .permission-globally-forced .session-permission-status { color: var(--warning, #d49a55); }
570
595
  .permission-label-short { display: none; }
571
596
  @media (max-width: 760px) {
572
597
  .permission-label-long { display: none; }
573
598
  .permission-label-short { display: inline; }
574
- .session-permission-control { column-gap: 5px; row-gap: 2px; }
599
+ .session-permission-control { column-gap: 4px; }
575
600
  .session-permission-segmented button { padding-inline: 7px; }
576
601
  .session-permission-status {
577
- flex-basis: 96px;
578
- max-width: 170px;
602
+ max-width: 132px;
579
603
  font-size: 9px;
580
604
  }
581
605
  }
606
+ @media (prefers-reduced-motion: reduce) {
607
+ .session-permission-segmented button { transition: none; }
608
+ .session-permission-segmented button.pending .permission-spinner { animation: none; opacity: .7; }
609
+ }
582
610
  #skip-perms-pill .lucide { width: 12px; height: 12px; flex-shrink: 0; }
583
611
 
584
612
  /* --- Confirm modal --- */
@@ -370,12 +370,12 @@ body.pane-mode #input-area {
370
370
  tie and keep the pill at its 22px title-bar size inside the 24px pane
371
371
  header. Scope under .split-pane-header to outrank them. */
372
372
  .split-pane-header .split-pane-full-access {
373
- height: auto;
373
+ height: 22px;
374
374
  align-self: center;
375
375
  margin-left: 0;
376
376
  padding: 0;
377
377
  gap: 4px;
378
- font-size: 8px;
378
+ font-size: 10px;
379
379
  letter-spacing: 0.02em;
380
380
  flex-wrap: nowrap;
381
381
  flex: 0 0 auto;
@@ -383,10 +383,14 @@ body.pane-mode #input-area {
383
383
  }
384
384
 
385
385
  .split-pane-header .session-permission-segmented button {
386
- padding: 1px 5px;
387
- font-size: 8px;
386
+ min-height: 18px;
387
+ padding: 3px 6px;
388
+ font-size: 10px;
388
389
  }
389
390
 
391
+ .split-pane-header .permission-label-long { display: none; }
392
+ .split-pane-header .permission-label-short { display: inline; }
393
+
390
394
  .split-pane-header .session-permission-status {
391
395
  flex: 0 0 auto;
392
396
  min-width: 40px;
@@ -394,7 +398,7 @@ body.pane-mode #input-area {
394
398
  overflow: hidden;
395
399
  text-overflow: ellipsis;
396
400
  white-space: nowrap;
397
- font-size: 8px;
401
+ font-size: 10px;
398
402
  }
399
403
 
400
404
  .split-pane-header .session-full-access-track {
@@ -410,9 +410,9 @@
410
410
  <button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
411
411
  <div id="header-full-access-btn" class="session-permission-control hidden" role="group" aria-label="Session permission mode">
412
412
  <div class="session-permission-segmented">
413
- <button type="button" data-permission-mode="default">Ask</button>
414
- <button type="button" data-permission-mode="auto">Auto</button>
415
- <button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label-long">Skip permissions</span><span class="permission-label-short" aria-hidden="true">Skip</span></button>
413
+ <button type="button" data-permission-mode="default"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>
414
+ <button type="button" data-permission-mode="auto"><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>
415
+ <button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions" title="Skip permissions"><span class="permission-label-long permission-label">Skip permissions</span><span class="permission-label-short permission-label" aria-hidden="true">Skip</span><span class="permission-spinner" aria-hidden="true"></span></button>
416
416
  </div>
417
417
  <span class="session-permission-status hidden" aria-live="polite"></span>
418
418
  </div>
@@ -30,6 +30,7 @@ import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.
30
30
  import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
31
31
  import { handleSessionActionMessage } from './session-actions.js';
32
32
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
33
+ import { renderDriverContinuation, updateDriverContinuation, renderDriverContinuationContext } from './driver-continuation.js';
33
34
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent, handleAcceptedOrdinaryMessage, handleScheduleMessageResult } from './input.js';
34
35
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
35
36
  import { finishThinkingTurn, resumeThinkingAfterReplay } from './thinking-lifecycle.js';
@@ -756,6 +757,19 @@ export function processMessage(msg) {
756
757
  updateWorkerProposal(msg);
757
758
  break;
758
759
 
760
+ case "driver_continuation_proposal":
761
+ renderDriverContinuation(msg);
762
+ break;
763
+
764
+ case "driver_continuation_update":
765
+ case "driver_continuation_result":
766
+ updateDriverContinuation(msg);
767
+ break;
768
+
769
+ case "driver_continuation_context":
770
+ renderDriverContinuationContext(msg);
771
+ break;
772
+
759
773
  case "split_delegation":
760
774
  handleSplitDelegation(msg);
761
775
  syncPaneTitles();
@@ -0,0 +1,76 @@
1
+ function clean(value) {
2
+ return value === undefined || value === null ? "" : String(value);
3
+ }
4
+
5
+ export function continuationKey(message) {
6
+ if (!message) return "";
7
+ var projectSlug = clean(message.projectSlug);
8
+ var sourceOriginId = clean(message.sourceOriginId);
9
+ var proposalId = clean(message.proposalId);
10
+ if (!projectSlug || !sourceOriginId || !proposalId) return "";
11
+ return projectSlug + "\n" + sourceOriginId + "\n" + proposalId;
12
+ }
13
+
14
+ export function authoritativeState(message) {
15
+ return {
16
+ key: continuationKey(message),
17
+ projectSlug: clean(message.projectSlug),
18
+ sourceOriginId: clean(message.sourceOriginId),
19
+ proposalId: clean(message.proposalId),
20
+ sourceSessionId: Number(message.sourceSessionId),
21
+ status: message.status || "pending",
22
+ requestId: "",
23
+ requestKind: "",
24
+ inflight: false,
25
+ error: message.error || "",
26
+ };
27
+ }
28
+
29
+ export function beginRequest(state, kind, requestId, connected) {
30
+ if (!state || !state.key || state.inflight || connected !== true) return null;
31
+ if (kind === "decision" && state.status !== "pending") return null;
32
+ var next = Object.assign({}, state, {
33
+ inflight: true,
34
+ requestId: clean(requestId),
35
+ requestKind: kind,
36
+ error: "",
37
+ });
38
+ return next.requestId ? next : null;
39
+ }
40
+
41
+ export function applyResponse(state, message) {
42
+ if (!state || !message || continuationKey(message) !== state.key) return null;
43
+ if (message.requestId && clean(message.requestId) !== state.requestId) return null;
44
+ var next = Object.assign({}, state, { inflight: false, requestId: "", requestKind: "" });
45
+ var sourceSessionId = Number(message.sourceSessionId);
46
+ if (Number.isInteger(sourceSessionId) && sourceSessionId > 0) next.sourceSessionId = sourceSessionId;
47
+ if (message.status) next.status = message.status;
48
+ next.error = message.error || "";
49
+ return next;
50
+ }
51
+
52
+ export function failRequest(state, requestId, error) {
53
+ if (!state || !state.inflight || clean(requestId) !== state.requestId) return null;
54
+ return Object.assign({}, state, {
55
+ inflight: false,
56
+ requestId: "",
57
+ requestKind: "",
58
+ error: clean(error) || "The request could not be sent. Try again.",
59
+ });
60
+ }
61
+
62
+ export function sendContinuationPayload(ws, payload) {
63
+ if (!ws || ws.readyState !== 1) return { ok: false, error: "The connection is unavailable. Try again." };
64
+ try {
65
+ if (ws.send(JSON.stringify(payload)) === false) {
66
+ return { ok: false, error: "The request was not accepted by the connection. Try again." };
67
+ }
68
+ } catch (error) {
69
+ return { ok: false, error: "The request could not be sent. Try again." };
70
+ }
71
+ return { ok: true };
72
+ }
73
+
74
+ export function continuationCanAct(state, connected) {
75
+ return !!state && state.status === "pending" && !state.inflight && connected === true;
76
+ }
@@ -0,0 +1,260 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { addToMessages, scrollToBottom } from './app-rendering.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { continuationKey, authoritativeState, beginRequest, applyResponse, failRequest, sendContinuationPayload, continuationCanAct } from './driver-continuation-state.js';
6
+
7
+ var REQUEST_TIMEOUT_MS = 12000;
8
+
9
+ function states() {
10
+ return store.get("driverContinuations") || {};
11
+ }
12
+
13
+ function stateFor(key) {
14
+ return states()[key] || null;
15
+ }
16
+
17
+ function setState(key, value) {
18
+ var next = Object.assign({}, states());
19
+ next[key] = value;
20
+ store.set({ driverContinuations: next });
21
+ }
22
+
23
+ function currentProjectAllows(message) {
24
+ var current = store.get("currentSlug");
25
+ return !current || !message.projectSlug || current === message.projectSlug;
26
+ }
27
+
28
+ function hydrate(message) {
29
+ if (!currentProjectAllows(message)) return null;
30
+ var normalized = message;
31
+ var activeSessionId = store.get("activeSessionId");
32
+ if (message.type === "driver_continuation_proposal" && activeSessionId !== undefined && activeSessionId !== null) {
33
+ normalized = Object.assign({}, message, { sourceSessionId: activeSessionId });
34
+ }
35
+ var state = authoritativeState(normalized);
36
+ if (!state.key) return null;
37
+ setState(state.key, state);
38
+ return state;
39
+ }
40
+
41
+ function cardsFor(key) {
42
+ var cards = document.querySelectorAll("[data-driver-continuation-key]");
43
+ var found = [];
44
+ for (var i = 0; i < cards.length; i++) {
45
+ if (cards[i].dataset.driverContinuationKey === key) found.push(cards[i]);
46
+ }
47
+ return found;
48
+ }
49
+
50
+ function statusLabel(status) {
51
+ if (status === "starting") return "Starting new session";
52
+ if (status === "accepted") return "Continued in new session";
53
+ if (status === "declined") return "Staying here";
54
+ if (status === "superseded") return "Work changed";
55
+ return "Your choice";
56
+ }
57
+
58
+ function syncCard(card) {
59
+ var state = stateFor(card.dataset.driverContinuationKey);
60
+ if (!state) return;
61
+ card.dataset.status = state.status;
62
+ card.dataset.inflight = state.inflight ? "true" : "false";
63
+ var badge = card.querySelector(".driver-continuation-status");
64
+ var error = card.querySelector(".driver-continuation-error");
65
+ if (badge) badge.textContent = statusLabel(state.status);
66
+ if (error) {
67
+ error.textContent = state.error;
68
+ error.hidden = !state.error;
69
+ }
70
+ var enabled = continuationCanAct(state, store.get("connected"));
71
+ var buttons = card.querySelectorAll(".driver-continuation-action");
72
+ for (var i = 0; i < buttons.length; i++) buttons[i].disabled = !enabled;
73
+ var sourceButton = card.querySelector(".driver-continuation-source");
74
+ if (sourceButton) sourceButton.disabled = state.inflight || store.get("connected") !== true;
75
+ }
76
+
77
+ function syncKey(key) {
78
+ var cards = cardsFor(key);
79
+ for (var i = 0; i < cards.length; i++) syncCard(cards[i]);
80
+ }
81
+
82
+ function requestId() {
83
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") return globalThis.crypto.randomUUID();
84
+ return "continuation_" + Date.now() + "_" + Math.random().toString(16).slice(2);
85
+ }
86
+
87
+ function fail(key, exactRequestId, message) {
88
+ var next = failRequest(stateFor(key), exactRequestId, message);
89
+ if (!next) return false;
90
+ setState(key, next);
91
+ return true;
92
+ }
93
+
94
+ function sendRequest(key, kind, payload) {
95
+ var ws = getWs();
96
+ var id = requestId();
97
+ var next = beginRequest(stateFor(key), kind, id, store.get("connected"));
98
+ if (!next || !ws || ws.readyState !== 1) return false;
99
+ setState(key, next);
100
+ payload.requestId = id;
101
+ payload.projectSlug = next.projectSlug;
102
+ payload.sourceOriginId = next.sourceOriginId;
103
+ payload.proposalId = next.proposalId;
104
+ var sent = sendContinuationPayload(ws, payload);
105
+ if (!sent.ok) {
106
+ fail(key, id, sent.error);
107
+ return false;
108
+ }
109
+ setTimeout(function () {
110
+ fail(key, id, "Clay did not confirm the request. Check the connection and try again.");
111
+ }, REQUEST_TIMEOUT_MS);
112
+ return true;
113
+ }
114
+
115
+ function sendChoice(card, accepted) {
116
+ sendRequest(card.dataset.driverContinuationKey, "decision", {
117
+ type: "driver_continuation_response",
118
+ accepted: accepted === true,
119
+ });
120
+ }
121
+
122
+ function addDetail(details, label, value) {
123
+ if (!value) return;
124
+ var term = document.createElement("dt");
125
+ term.textContent = label;
126
+ var description = document.createElement("dd");
127
+ description.textContent = value;
128
+ details.appendChild(term);
129
+ details.appendChild(description);
130
+ }
131
+
132
+ function evidenceText(evidence) {
133
+ if (!evidence) return "";
134
+ if (evidence.kind === "current_context_pressure" && typeof evidence.usedRatio === "number") {
135
+ var percent = Math.round(evidence.usedRatio * 100);
136
+ var threshold = Math.round(Number(evidence.thresholdRatio || 0) * 100);
137
+ return percent + "% of the current context is in use (proposal threshold: " + threshold + "%).";
138
+ }
139
+ if (evidence.kind === "recorded_compaction") {
140
+ var count = Number(evidence.observedCount || 0);
141
+ return "Clay recorded " + count + " completed context compaction" + (count === 1 ? "" : "s") + " in this session.";
142
+ }
143
+ return "";
144
+ }
145
+
146
+ export function renderDriverContinuation(msg) {
147
+ var state = hydrate(msg);
148
+ if (!state) return;
149
+ var existing = cardsFor(state.key);
150
+ if (existing.length) { syncKey(state.key); return; }
151
+ var handoff = msg.handoff || {};
152
+ var card = document.createElement("section");
153
+ card.className = "driver-continuation-card";
154
+ card.dataset.driverContinuationKey = state.key;
155
+ card.innerHTML = '<header><span class="driver-continuation-mark">' + iconHtml("arrow-up-right") + '</span><div><span class="driver-continuation-kicker">DRIVER CONTINUATION</span><strong>Carry this work into a fresh session?</strong></div><span class="driver-continuation-status"></span></header>';
156
+ var reason = document.createElement("p");
157
+ reason.className = "driver-continuation-reason";
158
+ reason.textContent = msg.reason || "The Driver identified a safe boundary for a compact handoff.";
159
+ card.appendChild(reason);
160
+ var summary = document.createElement("div");
161
+ summary.className = "driver-continuation-summary";
162
+ addDetail(summary, "Observed evidence", evidenceText(msg.triggerEvidence));
163
+ addDetail(summary, "Completed milestone", msg.milestone);
164
+ addDetail(summary, "Why a fresh session helps", msg.benefit);
165
+ addDetail(summary, "Goal", handoff.goal);
166
+ addDetail(summary, "Next action", handoff.nextAction);
167
+ card.appendChild(summary);
168
+ var disclosure = document.createElement("details");
169
+ disclosure.className = "driver-continuation-details";
170
+ disclosure.innerHTML = "<summary>Review inherited context</summary>";
171
+ var details = document.createElement("dl");
172
+ addDetail(details, "Constraints", handoff.constraints);
173
+ addDetail(details, "Decisions and why", handoff.decisions);
174
+ addDetail(details, "Rejected approaches", handoff.rejectedApproaches);
175
+ addDetail(details, "Unresolved", handoff.unresolved);
176
+ addDetail(details, "Repository state", handoff.repositoryState);
177
+ addDetail(details, "Verification", handoff.verification);
178
+ disclosure.appendChild(details);
179
+ card.appendChild(disclosure);
180
+ var error = document.createElement("p");
181
+ error.className = "driver-continuation-error";
182
+ error.hidden = true;
183
+ card.appendChild(error);
184
+ var actions = document.createElement("div");
185
+ actions.className = "driver-continuation-actions";
186
+ var stay = document.createElement("button");
187
+ stay.type = "button";
188
+ stay.className = "driver-continuation-action secondary";
189
+ stay.textContent = "Stay here";
190
+ stay.addEventListener("click", function () { sendChoice(card, false); });
191
+ var proceed = document.createElement("button");
192
+ proceed.type = "button";
193
+ proceed.className = "driver-continuation-action primary";
194
+ proceed.innerHTML = iconHtml("arrow-right") + "<span>Continue in new session</span>";
195
+ proceed.addEventListener("click", function () { sendChoice(card, true); });
196
+ actions.appendChild(stay);
197
+ actions.appendChild(proceed);
198
+ card.appendChild(actions);
199
+ addToMessages(card);
200
+ syncCard(card);
201
+ refreshIcons();
202
+ scrollToBottom();
203
+ }
204
+
205
+ export function updateDriverContinuation(msg) {
206
+ if (!currentProjectAllows(msg)) return false;
207
+ var key = continuationKey(msg);
208
+ var current = stateFor(key);
209
+ if (!current) return false;
210
+ var next = applyResponse(current, msg);
211
+ if (!next) return false;
212
+ setState(key, next);
213
+ return true;
214
+ }
215
+
216
+ export function renderDriverContinuationContext(msg) {
217
+ var seed = Object.assign({ status: "accepted" }, msg);
218
+ var key = continuationKey(seed);
219
+ if (!stateFor(key)) hydrate(seed);
220
+ var card = document.createElement("section");
221
+ card.className = "driver-continuation-context";
222
+ card.dataset.driverContinuationKey = key;
223
+ card.innerHTML = '<span class="driver-continuation-mark">' + iconHtml("history") + '</span><div class="driver-continuation-context-copy"><span class="driver-continuation-kicker">INHERITED CONTEXT</span></div>';
224
+ var copy = card.querySelector(".driver-continuation-context-copy");
225
+ var title = document.createElement("strong");
226
+ title.textContent = msg.goal || "Continued Driver work";
227
+ var next = document.createElement("p");
228
+ next.textContent = "Next: " + (msg.nextAction || "Revalidate the current state and continue.");
229
+ var error = document.createElement("p");
230
+ error.className = "driver-continuation-error";
231
+ error.hidden = true;
232
+ var source = document.createElement("button");
233
+ source.type = "button";
234
+ source.className = "driver-continuation-source";
235
+ source.textContent = "Open original session";
236
+ source.addEventListener("click", function () {
237
+ sendRequest(key, "open_source", { type: "driver_continuation_open_source" });
238
+ });
239
+ copy.appendChild(title);
240
+ copy.appendChild(next);
241
+ copy.appendChild(error);
242
+ copy.appendChild(source);
243
+ addToMessages(card);
244
+ syncCard(card);
245
+ refreshIcons();
246
+ }
247
+
248
+ store.subscribe(function (state, previous) {
249
+ if (state.connected === previous.connected &&
250
+ state.driverContinuations === previous.driverContinuations) return;
251
+ if (state.connected === false && previous.connected === true) {
252
+ var current = states();
253
+ var keys = Object.keys(current);
254
+ for (var i = 0; i < keys.length; i++) {
255
+ if (current[keys[i]].inflight) fail(keys[i], current[keys[i]].requestId, "Connection lost before Clay confirmed the request. Try again after reconnecting.");
256
+ }
257
+ }
258
+ var cards = document.querySelectorAll("[data-driver-continuation-key]");
259
+ for (var j = 0; j < cards.length; j++) syncCard(cards[j]);
260
+ });
@@ -46,10 +46,11 @@ export function createPermissionControl(className, ariaLabel) {
46
46
  control.setAttribute("role", "group");
47
47
  control.setAttribute("aria-label", ariaLabel || "Session permission mode");
48
48
  control.innerHTML = '<div class="session-permission-segmented">' +
49
- '<button type="button" data-permission-mode="default">Ask</button>' +
50
- '<button type="button" data-permission-mode="auto">Auto</button>' +
51
- '<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label-long">Skip permissions</span><span class="permission-label-short" aria-hidden="true">Skip</span></button>' +
49
+ '<button type="button" data-permission-mode="default"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
50
+ '<button type="button" data-permission-mode="auto"><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
51
+ '<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions" title="Skip permissions"><span class="permission-label-long permission-label">Skip permissions</span><span class="permission-label-short permission-label" aria-hidden="true">Skip</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
52
52
  '</div><span class="session-permission-status" aria-live="polite"></span>';
53
+ control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true");
53
54
  return control;
54
55
  }
55
56
 
@@ -99,15 +100,19 @@ export function renderPermissionControl(control, state) {
99
100
  control.classList.toggle("permission-auto-unsupported", autoVisible && !autoSupported);
100
101
  control.classList.toggle("permission-globally-forced", globallyForced);
101
102
  control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
103
+ control.setAttribute("aria-busy", pending ? "true" : "false");
102
104
  var buttons = control.querySelectorAll("[data-permission-mode]");
103
105
  for (var i = 0; i < buttons.length; i++) {
104
106
  var mode = buttons[i].dataset.permissionMode;
105
107
  var unsupported = mode === "auto" && autoVisible && !autoSupported;
108
+ var requested = !!pending && pending.mode === mode;
106
109
  buttons[i].hidden = mode === "auto" && !autoVisible;
107
110
  var selected = globallyForced ? mode === "bypassPermissions" : mode === state.permissionMode;
108
111
  buttons[i].disabled = globallyForced || unsupported || !!pending || !connected || state.locked === true;
109
112
  buttons[i].classList.toggle("active", selected);
113
+ buttons[i].classList.toggle("pending", requested);
110
114
  buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
115
+ buttons[i].setAttribute("aria-busy", requested ? "true" : "false");
111
116
  if (unsupported) buttons[i].title = "Auto permissions are unavailable for this Claude session.";
112
117
  else buttons[i].removeAttribute("title");
113
118
  }
@@ -26,6 +26,7 @@
26
26
  @import url("css/loop.css");
27
27
  @import url("css/scheduler.css");
28
28
  @import url("css/project-logs.css");
29
+ @import url("css/driver-continuation.css");
29
30
  @import url("css/scheduled-tasks.css");
30
31
  @import url("css/scheduler-modal.css");
31
32
  @import url("css/home-hub.css");