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.
- package/lib/driver-continuation-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +82 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-pair-lifecycle.js +73 -73
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-user-message.js +14 -0
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project.js +49 -2
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/overlays.css +44 -16
- package/lib/public/css/pane.css +9 -5
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-messages.js +14 -0
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/permission-control.js +8 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-prompts.js +5 -0
- package/lib/session-pair-turn-control.js +19 -3
- package/lib/session-split-group-anchors.js +44 -0
- package/lib/session-split-groups.js +209 -59
- package/lib/sessions.js +20 -2
- package/lib/ws-schema.js +6 -0
- 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:
|
|
527
|
-
gap:
|
|
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:
|
|
536
|
+
gap: 1px;
|
|
537
537
|
padding: 2px;
|
|
538
|
-
border: 1px solid var(--border,
|
|
539
|
-
border-radius:
|
|
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:
|
|
546
|
-
padding:
|
|
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
|
|
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:
|
|
568
|
+
flex: 0 1 auto;
|
|
559
569
|
min-width: 0;
|
|
560
|
-
max-width:
|
|
561
|
-
overflow
|
|
562
|
-
|
|
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:
|
|
599
|
+
.session-permission-control { column-gap: 4px; }
|
|
575
600
|
.session-permission-segmented button { padding-inline: 7px; }
|
|
576
601
|
.session-permission-status {
|
|
577
|
-
|
|
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 --- */
|
package/lib/public/css/pane.css
CHANGED
|
@@ -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:
|
|
373
|
+
height: 22px;
|
|
374
374
|
align-self: center;
|
|
375
375
|
margin-left: 0;
|
|
376
376
|
padding: 0;
|
|
377
377
|
gap: 4px;
|
|
378
|
-
font-size:
|
|
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
|
-
|
|
387
|
-
|
|
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:
|
|
401
|
+
font-size: 10px;
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
.split-pane-header .session-full-access-track {
|
package/lib/public/index.html
CHANGED
|
@@ -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
|
}
|
package/lib/public/style.css
CHANGED
|
@@ -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");
|