clay-server 4.0.0-beta.4 → 4.0.0-beta.6
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/background-task-timing.js +34 -1
- package/lib/public/css/input.css +6 -0
- package/lib/public/index.html +2 -0
- package/lib/public/modules/app-messages.js +4 -4
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/background-tasks-ui.js +34 -5
- package/lib/sdk-message-processor.js +11 -4
- package/lib/sessions.js +4 -1
- package/lib/yoke/adapters/claude.js +63 -3
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Background task
|
|
1
|
+
// Background task list shaping shared by the server-side consumers:
|
|
2
|
+
// start-time tracking, plus the ambient/user split every activity indicator
|
|
3
|
+
// needs.
|
|
2
4
|
//
|
|
3
5
|
// Vendors report background tasks as a full replacement list on every change
|
|
4
6
|
// and none of them carries a reliable start time today (see
|
|
@@ -66,7 +68,38 @@ function mergeStartTimes(previousTasks, nextTasks, now) {
|
|
|
66
68
|
return merged;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// Ambient tasks are the CLI's own housekeeping (every skip_transcript task
|
|
72
|
+
// plus auto-started live-update watchers). The SDK asks hosts to keep them out
|
|
73
|
+
// of activity indicators, so every count that drives a badge, a summary, or a
|
|
74
|
+
// change notification must go through here rather than reading `.length`.
|
|
75
|
+
// Absent means false: vendors without an ambient concept (Codex terminals)
|
|
76
|
+
// and older CLI builds report only real user work.
|
|
77
|
+
function isAmbientTask(task) {
|
|
78
|
+
return !!(task && task.ambient === true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function userTasks(tasks) {
|
|
82
|
+
if (!Array.isArray(tasks)) return [];
|
|
83
|
+
return tasks.filter(function (task) {
|
|
84
|
+
return task && !isAmbientTask(task);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ambientTasks(tasks) {
|
|
89
|
+
if (!Array.isArray(tasks)) return [];
|
|
90
|
+
return tasks.filter(isAmbientTask);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The count that represents "user-visible background work is running".
|
|
94
|
+
function countUserTasks(tasks) {
|
|
95
|
+
return userTasks(tasks).length;
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
module.exports = {
|
|
70
99
|
mergeStartTimes: mergeStartTimes,
|
|
71
100
|
normalizeTimestamp: normalizeTimestamp,
|
|
101
|
+
isAmbientTask: isAmbientTask,
|
|
102
|
+
userTasks: userTasks,
|
|
103
|
+
ambientTasks: ambientTasks,
|
|
104
|
+
countUserTasks: countUserTasks,
|
|
72
105
|
};
|
package/lib/public/css/input.css
CHANGED
|
@@ -742,6 +742,12 @@
|
|
|
742
742
|
.background-task-icon { display: inline-flex; flex: none; color: var(--text-muted); }
|
|
743
743
|
.background-task-icon i { width: 13px; height: 13px; }
|
|
744
744
|
.background-task-description { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
|
|
745
|
+
/* Housekeeping group: visible so the user can see what Clay runs on its own,
|
|
746
|
+
but subordinate to the work they actually asked for. */
|
|
747
|
+
.background-tasks-section-label { padding: 9px 0 1px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
|
|
748
|
+
.background-tasks-section-label + .background-task-row { border-top: 0; }
|
|
749
|
+
.background-task-row.ambient .background-task-description { color: var(--text-muted); }
|
|
750
|
+
.background-task-row.ambient .background-task-icon { opacity: .7; }
|
|
745
751
|
.background-task-meta { display: inline-flex; align-items: center; gap: 7px; flex: none; color: var(--text-muted); }
|
|
746
752
|
.background-task-type { font-size: 11px; }
|
|
747
753
|
.background-task-elapsed { font-variant-numeric: tabular-nums; font-size: 11px; min-width: 44px; text-align: right; }
|
package/lib/public/index.html
CHANGED
|
@@ -501,12 +501,14 @@
|
|
|
501
501
|
<div class="usage-section-label">Session Tokens</div>
|
|
502
502
|
<div class="usage-row"><span class="usage-label">Input</span><span id="context-input" class="usage-value">0</span></div>
|
|
503
503
|
<div class="usage-row"><span class="usage-label">Output</span><span id="context-output" class="usage-value">0</span></div>
|
|
504
|
+
<div class="usage-row hidden" id="context-thinking-row" title="Extended thinking tokens, already counted inside Output"><span class="usage-label">Thinking</span><span id="context-thinking" class="usage-value">0</span></div>
|
|
504
505
|
<div class="usage-row"><span class="usage-label">Cache read</span><span id="context-cache-read" class="usage-value">0</span></div>
|
|
505
506
|
<div class="usage-row"><span class="usage-label">Cache write</span><span id="context-cache-write" class="usage-value">0</span></div>
|
|
506
507
|
<div class="usage-divider"></div>
|
|
507
508
|
<div class="usage-section-label">Model</div>
|
|
508
509
|
<div class="usage-row"><span class="usage-label">Name</span><span id="context-model" class="usage-value">-</span></div>
|
|
509
510
|
<div class="usage-row"><span class="usage-label">Cost</span><span id="context-cost" class="usage-value">$0.0000</span></div>
|
|
511
|
+
<div class="usage-row hidden" id="context-cost-basis-row" title="Which price table this cost was computed from"><span class="usage-label">Priced at</span><span id="context-cost-basis" class="usage-value">-</span></div>
|
|
510
512
|
<div class="usage-row"><span class="usage-label">Turns</span><span id="context-turns" class="usage-value">0</span></div>
|
|
511
513
|
</div>
|
|
512
514
|
</div>
|
|
@@ -1300,10 +1300,10 @@ export function processMessage(msg) {
|
|
|
1300
1300
|
appendDelta((msg.text || "Authentication required.") + "\n");
|
|
1301
1301
|
setStatus("connected");
|
|
1302
1302
|
if (!store.get('loopActive')) enableMainInput();
|
|
1303
|
-
// Open the login flow for
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
autoStartLoginIfNeeded(msg);
|
|
1303
|
+
// Open the login flow only for a live auth failure. Older Clay
|
|
1304
|
+
// versions recorded this transient event in session history; replaying
|
|
1305
|
+
// one must not reopen a login terminal after the user signed in.
|
|
1306
|
+
if (!store.get('replayingHistory')) autoStartLoginIfNeeded(msg);
|
|
1307
1307
|
break;
|
|
1308
1308
|
|
|
1309
1309
|
case "vendor_login_ready":
|
|
@@ -10,7 +10,7 @@ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelP
|
|
|
10
10
|
|
|
11
11
|
// --- Module-owned state (not in store) ---
|
|
12
12
|
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
13
|
-
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
13
|
+
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
14
14
|
var ctxPopoverEl = null;
|
|
15
15
|
var ctxHoverTimer = null;
|
|
16
16
|
var statusRefreshTimer = null;
|
|
@@ -72,6 +72,10 @@ var contextCacheReadEl = null;
|
|
|
72
72
|
var contextCacheWriteEl = null;
|
|
73
73
|
var contextModelEl = null;
|
|
74
74
|
var contextCostEl = null;
|
|
75
|
+
var contextThinkingEl = null;
|
|
76
|
+
var contextThinkingRow = null;
|
|
77
|
+
var contextCostBasisEl = null;
|
|
78
|
+
var contextCostBasisRow = null;
|
|
75
79
|
var contextTurnsEl = null;
|
|
76
80
|
var contextMini = null;
|
|
77
81
|
var contextMiniFill = null;
|
|
@@ -449,6 +453,10 @@ export function initPanels() {
|
|
|
449
453
|
contextCacheWriteEl = $("context-cache-write");
|
|
450
454
|
contextModelEl = $("context-model");
|
|
451
455
|
contextCostEl = $("context-cost");
|
|
456
|
+
contextThinkingEl = $("context-thinking");
|
|
457
|
+
contextThinkingRow = $("context-thinking-row");
|
|
458
|
+
contextCostBasisEl = $("context-cost-basis");
|
|
459
|
+
contextCostBasisRow = $("context-cost-basis-row");
|
|
452
460
|
contextTurnsEl = $("context-turns");
|
|
453
461
|
contextMini = $("context-mini");
|
|
454
462
|
contextMiniFill = $("context-mini-fill");
|
|
@@ -776,6 +784,16 @@ export function updateContextPanel() {
|
|
|
776
784
|
contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
|
|
777
785
|
contextModelEl.textContent = contextData.model;
|
|
778
786
|
contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
|
|
787
|
+
// Optional SDK fields: shown only once the CLI has actually reported them.
|
|
788
|
+
if (contextThinkingRow && contextThinkingEl) {
|
|
789
|
+
var hasThinking = typeof contextData.thinking === "number";
|
|
790
|
+
contextThinkingRow.classList.toggle("hidden", !hasThinking);
|
|
791
|
+
if (hasThinking) contextThinkingEl.textContent = formatTokens(contextData.thinking);
|
|
792
|
+
}
|
|
793
|
+
if (contextCostBasisRow && contextCostBasisEl) {
|
|
794
|
+
contextCostBasisRow.classList.toggle("hidden", !contextData.costBasis);
|
|
795
|
+
if (contextData.costBasis) contextCostBasisEl.textContent = contextData.costBasis;
|
|
796
|
+
}
|
|
779
797
|
contextTurnsEl.textContent = String(contextData.turns);
|
|
780
798
|
}
|
|
781
799
|
|
|
@@ -811,6 +829,29 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
811
829
|
contextData.model = displayModel;
|
|
812
830
|
contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
|
|
813
831
|
if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
|
|
832
|
+
|
|
833
|
+
// thinkingTokens and costBasis are per-model and optional: absent on
|
|
834
|
+
// older CLI builds and, for a resumed session, on the turns that ran
|
|
835
|
+
// before the field existed. Leave them null in that case so the rows
|
|
836
|
+
// stay hidden rather than claiming a confident zero.
|
|
837
|
+
var thinkingTotal = null;
|
|
838
|
+
var basis = null;
|
|
839
|
+
var basisMixed = false;
|
|
840
|
+
for (var mi = 0; mi < models.length; mi++) {
|
|
841
|
+
var entry = modelUsage[models[mi]];
|
|
842
|
+
if (!entry) continue;
|
|
843
|
+
if (typeof entry.thinkingTokens === "number") {
|
|
844
|
+
thinkingTotal = (thinkingTotal || 0) + entry.thinkingTokens;
|
|
845
|
+
}
|
|
846
|
+
if (entry.costBasis) {
|
|
847
|
+
if (basis === null) basis = entry.costBasis;
|
|
848
|
+
else if (basis !== entry.costBasis) basisMixed = true;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
contextData.thinking = thinkingTotal;
|
|
852
|
+
// Several models in one query can be priced differently; say so rather
|
|
853
|
+
// than reporting whichever happened to come first.
|
|
854
|
+
contextData.costBasis = basisMixed ? "mixed" : basis;
|
|
814
855
|
}
|
|
815
856
|
}
|
|
816
857
|
if (!store.get('replayingHistory')) updateContextPanel();
|
|
@@ -832,7 +873,7 @@ export function applyContextView(view) {
|
|
|
832
873
|
}
|
|
833
874
|
|
|
834
875
|
export function resetContextData() {
|
|
835
|
-
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
876
|
+
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
836
877
|
store.set({ richContextUsage: null });
|
|
837
878
|
// hideCtxPopover + updateContextPanel handled by store subscriber
|
|
838
879
|
}
|
|
@@ -76,13 +76,33 @@ function activityDotsHtml() {
|
|
|
76
76
|
return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Ambient tasks are the CLI's own housekeeping. The SDK asks hosts to keep
|
|
80
|
+
// them out of activity indicators; Clay still lists them so the user can see
|
|
81
|
+
// what runs on their behalf. Absent means false (Codex has no ambient concept,
|
|
82
|
+
// and older CLI builds report only user work).
|
|
83
|
+
export function isAmbientTask(task) {
|
|
84
|
+
return !!(task && task.ambient === true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function splitTasks(tasks) {
|
|
88
|
+
var user = [];
|
|
89
|
+
var ambient = [];
|
|
90
|
+
var list = Array.isArray(tasks) ? tasks : [];
|
|
91
|
+
for (var i = 0; i < list.length; i++) {
|
|
92
|
+
if (!list[i]) continue;
|
|
93
|
+
if (isAmbientTask(list[i])) ambient.push(list[i]);
|
|
94
|
+
else user.push(list[i]);
|
|
95
|
+
}
|
|
96
|
+
return { user: user, ambient: ambient };
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
function taskRowHtml(task, now) {
|
|
80
100
|
var taskType = task.task_type || 'other';
|
|
81
101
|
var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
|
|
82
102
|
var description = task.description || 'Background task';
|
|
83
103
|
var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
|
|
84
104
|
var elapsed = formatElapsed(startedAt, now);
|
|
85
|
-
return '<div class="background-task-row">' +
|
|
105
|
+
return '<div class="background-task-row' + (isAmbientTask(task) ? ' ambient' : '') + '">' +
|
|
86
106
|
'<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
|
|
87
107
|
'<span class="background-task-description" title="' + escapeHtml(description) + '">' +
|
|
88
108
|
escapeHtml(description) + '</span>' +
|
|
@@ -97,8 +117,11 @@ function taskRowHtml(task, now) {
|
|
|
97
117
|
|
|
98
118
|
function renderBackgroundTasks() {
|
|
99
119
|
var tasks = store.get('activeBackgroundTasks') || [];
|
|
120
|
+
var split = splitTasks(tasks);
|
|
100
121
|
var existing = document.getElementById('background-tasks-bar');
|
|
101
|
-
|
|
122
|
+
// Ambient-only means nothing the user asked for is running, so the composer
|
|
123
|
+
// stays quiet: no bar, no dots, no count.
|
|
124
|
+
if (split.user.length === 0) {
|
|
102
125
|
if (existing) existing.remove();
|
|
103
126
|
expanded = false;
|
|
104
127
|
stopElapsedTimer();
|
|
@@ -111,8 +134,8 @@ function renderBackgroundTasks() {
|
|
|
111
134
|
bar.id = 'background-tasks-bar';
|
|
112
135
|
bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
|
|
113
136
|
|
|
114
|
-
var taskLabel =
|
|
115
|
-
var summary =
|
|
137
|
+
var taskLabel = split.user.length === 1 ? 'background task' : 'background tasks';
|
|
138
|
+
var summary = split.user.length + ' ' + taskLabel;
|
|
116
139
|
var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
|
|
117
140
|
' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
|
|
118
141
|
(expanded ? 'collapse' : 'expand') + ' details">' +
|
|
@@ -124,7 +147,13 @@ function renderBackgroundTasks() {
|
|
|
124
147
|
if (expanded) {
|
|
125
148
|
var now = Date.now();
|
|
126
149
|
html += '<div class="background-tasks-list" id="background-tasks-list">';
|
|
127
|
-
for (var i = 0; i <
|
|
150
|
+
for (var i = 0; i < split.user.length; i++) html += taskRowHtml(split.user[i], now);
|
|
151
|
+
// Housekeeping is shown, not hidden: the user can see what Clay runs on
|
|
152
|
+
// its own and stop a runaway one. Kept visually subordinate to real work.
|
|
153
|
+
if (split.ambient.length > 0) {
|
|
154
|
+
html += '<div class="background-tasks-section-label">Housekeeping</div>';
|
|
155
|
+
for (var a = 0; a < split.ambient.length; a++) html += taskRowHtml(split.ambient[a], now);
|
|
156
|
+
}
|
|
128
157
|
html += '</div>';
|
|
129
158
|
}
|
|
130
159
|
bar.innerHTML = html;
|
|
@@ -70,7 +70,10 @@ function attachMessageProcessor(ctx) {
|
|
|
70
70
|
var authTitle = ((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in.";
|
|
71
71
|
var loginCommand = (vendorInfo && vendorInfo.loginCommand) || "claude login";
|
|
72
72
|
var _nmLogin = getNotificationsModule();
|
|
73
|
-
|
|
73
|
+
// Authentication recovery is transient UI state, not transcript content.
|
|
74
|
+
// Recording it makes history replay look like a fresh auth failure and
|
|
75
|
+
// re-opens the login terminal after a successful sign-in.
|
|
76
|
+
sendToSession(session, {
|
|
74
77
|
type: "auth_required",
|
|
75
78
|
text: authTitle,
|
|
76
79
|
vendor: session.vendor || "claude",
|
|
@@ -193,7 +196,9 @@ function attachMessageProcessor(ctx) {
|
|
|
193
196
|
|
|
194
197
|
// Cache slash_commands and model from CLI init message
|
|
195
198
|
if (parsed.yokeType === "init") {
|
|
196
|
-
|
|
199
|
+
// Ambient housekeeping never drove the sidebar badge, so a reset that
|
|
200
|
+
// only clears ambient watchers must not rebroadcast the session list.
|
|
201
|
+
var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(session.activeBackgroundTasks);
|
|
197
202
|
session.activeBackgroundTasks = [];
|
|
198
203
|
sendToSession(session, { type: "active_background_tasks", tasks: [] });
|
|
199
204
|
if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
|
|
@@ -688,7 +693,9 @@ function attachMessageProcessor(ctx) {
|
|
|
688
693
|
|
|
689
694
|
} else if (parsed.yokeType === "background_tasks_changed") {
|
|
690
695
|
var previousBackgroundTasks = session.activeBackgroundTasks || [];
|
|
691
|
-
|
|
696
|
+
// Count user work only: an ambient watcher starting or stopping must not
|
|
697
|
+
// rebroadcast the session list and flicker every client's sidebar badge.
|
|
698
|
+
var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(previousBackgroundTasks);
|
|
692
699
|
// Vendors re-send the whole list on every change, so carry each task's
|
|
693
700
|
// first-seen time forward; otherwise the composer's elapsed timer would
|
|
694
701
|
// restart whenever any other task starts or finishes.
|
|
@@ -698,7 +705,7 @@ function attachMessageProcessor(ctx) {
|
|
|
698
705
|
);
|
|
699
706
|
session.activeBackgroundTasks = activeBackgroundTasks;
|
|
700
707
|
sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
|
|
701
|
-
if (previousBackgroundTaskCount !== activeBackgroundTasks
|
|
708
|
+
if (previousBackgroundTaskCount !== backgroundTaskTiming.countUserTasks(activeBackgroundTasks)) {
|
|
702
709
|
sm.broadcastSessionList();
|
|
703
710
|
}
|
|
704
711
|
|
package/lib/sessions.js
CHANGED
|
@@ -5,6 +5,7 @@ var utils = require("./utils");
|
|
|
5
5
|
var users = require("./users");
|
|
6
6
|
var { CODEX_DEFAULTS } = require("./codex-defaults");
|
|
7
7
|
var hygiene = require("./session-hygiene");
|
|
8
|
+
var backgroundTaskTiming = require("./background-task-timing");
|
|
8
9
|
|
|
9
10
|
function createSessionManager(opts) {
|
|
10
11
|
var cwd = opts.cwd;
|
|
@@ -508,7 +509,9 @@ function createSessionManager(opts) {
|
|
|
508
509
|
title: s.title || "New Session",
|
|
509
510
|
active: isActive,
|
|
510
511
|
isProcessing: s.isProcessing,
|
|
511
|
-
|
|
512
|
+
// Sidebar badge: user work only. Ambient housekeeping is surfaced in the
|
|
513
|
+
// composer's expanded list, never as a count demanding attention.
|
|
514
|
+
backgroundTaskCount: backgroundTaskTiming.countUserTasks(s.activeBackgroundTasks),
|
|
512
515
|
lastActivity: s.lastActivity || s.createdAt || 0,
|
|
513
516
|
loop: loop,
|
|
514
517
|
spawn: s.spawn || null,
|
|
@@ -20,6 +20,49 @@ var claudeUserInput = require("../claude-user-input");
|
|
|
20
20
|
// emitted. Keep in sync with the client's generic user-dialog renderer.
|
|
21
21
|
var SUPPORTED_DIALOG_KINDS = ["refusal_fallback_prompt"];
|
|
22
22
|
|
|
23
|
+
// Clay renders a per-task Stop control in the composer's background-tasks bar
|
|
24
|
+
// (public/modules/background-tasks-ui.js), wired through project-sessions.js
|
|
25
|
+
// `stop_task` -> adapter.stopTask -> rawQuery.stopTask (worker path: the
|
|
26
|
+
// stop_task message in claude-worker.js). Declaring that affordance makes an
|
|
27
|
+
// interrupt spare running background tasks so Stop only aborts the current
|
|
28
|
+
// turn. The CLI fails CLOSED on absence: without this, every interrupt would
|
|
29
|
+
// also kill background tasks, since a consumer with no per-task control could
|
|
30
|
+
// not otherwise stop a runaway one.
|
|
31
|
+
//
|
|
32
|
+
// Declared identically on the in-process and OS-user worker paths. The CLI
|
|
33
|
+
// takes first-attached-client-wins on multi-client sessions, so the two paths
|
|
34
|
+
// must not disagree, or which one connected first would decide whether Stop
|
|
35
|
+
// kills background work.
|
|
36
|
+
var PER_TASK_STOP_AFFORDANCE = true;
|
|
37
|
+
|
|
38
|
+
// --- System prompt recording (SDK `snapshot`) ---
|
|
39
|
+
// Records the conversation's system prompt once and replays it verbatim on
|
|
40
|
+
// every later request and resume, which keeps the prompt-cache prefix and
|
|
41
|
+
// extended-thinking context stable across CLI upgrades and flag changes.
|
|
42
|
+
//
|
|
43
|
+
// OFF for Clay's persistent sessions, deliberately. getSessionSystemPrompt
|
|
44
|
+
// (project.js) recomposes the append from live state every time the query is
|
|
45
|
+
// rebuilt - sticky notes, pair/Driver role, and the Mate Capsule catalog - and
|
|
46
|
+
// three call sites exist purely to force that re-render mid-conversation
|
|
47
|
+
// (project-capsule-catalog.js refreshSessions, project.js pair regrouping,
|
|
48
|
+
// sdk-bridge refreshEnvironmentRuntime). Because Clay resumes those
|
|
49
|
+
// conversations, a recorded prompt would pin the first render and silently
|
|
50
|
+
// swallow every later update until compaction.
|
|
51
|
+
//
|
|
52
|
+
// Callers whose prompt is fixed for the life of the conversation opt in with
|
|
53
|
+
// queryOpts.systemPromptSnapshot. A bare string prompt cannot be recorded, so
|
|
54
|
+
// it is promoted to the custom form the SDK requires.
|
|
55
|
+
function applySystemPromptSnapshot(systemPrompt, enabled) {
|
|
56
|
+
if (!enabled || !systemPrompt) return systemPrompt;
|
|
57
|
+
if (typeof systemPrompt === "string" || Array.isArray(systemPrompt)) {
|
|
58
|
+
return { type: "custom", prompt: systemPrompt, snapshot: true };
|
|
59
|
+
}
|
|
60
|
+
if (typeof systemPrompt === "object") {
|
|
61
|
+
return Object.assign({}, systemPrompt, { snapshot: true });
|
|
62
|
+
}
|
|
63
|
+
return systemPrompt;
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
function getSharedSkillPlugins(homeDir, existingPlugins) {
|
|
24
67
|
var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
|
|
25
68
|
var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(homeDir ? { homeDir: homeDir } : null);
|
|
@@ -45,10 +88,16 @@ function buildWorkerQueryOptions(queryOpts, claudeOpts, workerCwd) {
|
|
|
45
88
|
if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
|
|
46
89
|
if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
|
|
47
90
|
if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
|
|
48
|
-
if (queryOpts.appendSystemPrompt)
|
|
91
|
+
if (queryOpts.appendSystemPrompt) {
|
|
92
|
+
queryOptions.systemPrompt = applySystemPromptSnapshot(
|
|
93
|
+
{ type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
|
|
94
|
+
queryOpts.systemPromptSnapshot
|
|
95
|
+
);
|
|
96
|
+
}
|
|
49
97
|
queryOptions.plugins = queryOpts.skipSkills ? (claudeOpts.plugins || []) : getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins);
|
|
50
98
|
if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
|
|
51
99
|
if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
|
|
100
|
+
queryOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
|
|
52
101
|
if (queryOpts.model) queryOptions.model = queryOpts.model;
|
|
53
102
|
if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
|
|
54
103
|
if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
|
|
@@ -404,6 +453,11 @@ function normalizeBackgroundTasks(tasks) {
|
|
|
404
453
|
task_id: task && task.task_id,
|
|
405
454
|
task_type: taskType,
|
|
406
455
|
description: (task && task.description) || "",
|
|
456
|
+
// Housekeeping the CLI does not surface as user work (skip_transcript
|
|
457
|
+
// tasks, auto-started live-update watchers). Clay keeps these out of its
|
|
458
|
+
// activity indicators and lists them as a subdued secondary group.
|
|
459
|
+
// Normalized to a boolean so consumers never have to test for absence.
|
|
460
|
+
ambient: !!(task && task.ambient),
|
|
407
461
|
};
|
|
408
462
|
// The SDK does not report a start time today. Forward one if a future
|
|
409
463
|
// version adds it; otherwise the session stamps first-seen time.
|
|
@@ -1454,12 +1508,17 @@ function createClaudeAdapter(opts) {
|
|
|
1454
1508
|
if (_claudeBinaryPath) sdkOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
|
|
1455
1509
|
|
|
1456
1510
|
// YOKE standard options -> SDK options
|
|
1457
|
-
if (queryOpts.systemPrompt)
|
|
1511
|
+
if (queryOpts.systemPrompt) {
|
|
1512
|
+
sdkOptions.systemPrompt = applySystemPromptSnapshot(queryOpts.systemPrompt, queryOpts.systemPromptSnapshot);
|
|
1513
|
+
}
|
|
1458
1514
|
// Additive variant: keep the full Claude Code system prompt and append.
|
|
1459
1515
|
// A plain-string systemPrompt REPLACES the whole prompt, so pair
|
|
1460
1516
|
// presets and other add-ons must come through here instead.
|
|
1461
1517
|
if (!queryOpts.systemPrompt && queryOpts.appendSystemPrompt) {
|
|
1462
|
-
sdkOptions.systemPrompt =
|
|
1518
|
+
sdkOptions.systemPrompt = applySystemPromptSnapshot(
|
|
1519
|
+
{ type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
|
|
1520
|
+
queryOpts.systemPromptSnapshot
|
|
1521
|
+
);
|
|
1463
1522
|
}
|
|
1464
1523
|
if (queryOpts.model) sdkOptions.model = queryOpts.model;
|
|
1465
1524
|
if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
|
|
@@ -1475,6 +1534,7 @@ function createClaudeAdapter(opts) {
|
|
|
1475
1534
|
sdkOptions.onUserDialog = queryOpts.onUserDialog;
|
|
1476
1535
|
sdkOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
|
|
1477
1536
|
}
|
|
1537
|
+
sdkOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
|
|
1478
1538
|
if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
|
|
1479
1539
|
if (queryOpts.persistSession === false) sdkOptions.persistSession = false;
|
|
1480
1540
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.6",
|
|
4
4
|
"description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"homepage": "https://github.com/chadbyte/clay#readme",
|
|
50
50
|
"author": "Chad",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
52
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.258",
|
|
53
53
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
54
54
|
"@openai/codex": "^0.152.1",
|
|
55
55
|
"@seald-io/nedb": "^4.1.2",
|