clay-server 4.0.0-beta.24 → 4.0.0-beta.25

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/public/app.js CHANGED
@@ -377,6 +377,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
377
377
  currentMsgEl: null,
378
378
  currentFullText: "",
379
379
  matePreThinkingEl: null,
380
+ thinkingState: null,
380
381
  // panels
381
382
  fileViewerFullscreen: false,
382
383
  markdownSlidesActive: false,
@@ -693,6 +693,8 @@ pre.mermaid-error {
693
693
 
694
694
  .thinking-header {
695
695
  display: inline-flex;
696
+ max-width: 100%;
697
+ box-sizing: border-box;
696
698
  align-items: center;
697
699
  gap: 6px;
698
700
  cursor: pointer;
@@ -726,9 +728,44 @@ pre.mermaid-error {
726
728
  .thinking-label {
727
729
  font-size: 13px;
728
730
  color: var(--text-muted);
731
+ min-width: 0;
732
+ overflow: hidden;
733
+ text-overflow: ellipsis;
734
+ white-space: nowrap;
729
735
  }
730
736
 
737
+ .thinking-live .thinking-header {
738
+ padding: 6px 0;
739
+ background: transparent;
740
+ border-radius: 0;
741
+ }
742
+
743
+ .thinking-live .thinking-chevron { display: none; }
744
+
745
+ .thinking-live .thinking-label {
746
+ background: linear-gradient(100deg, var(--text-muted) 40%, var(--text-dimmer) 50%, var(--text-muted) 60%);
747
+ background-size: 300% 100%;
748
+ background-clip: text;
749
+ -webkit-background-clip: text;
750
+ color: transparent;
751
+ animation: thinking-summary-shimmer 2.8s ease-in-out infinite;
752
+ }
753
+
754
+ @keyframes thinking-summary-shimmer {
755
+ from { background-position: 100% 0; }
756
+ to { background-position: 0% 0; }
757
+ }
758
+
759
+ @media (prefers-reduced-motion: reduce) {
760
+ .thinking-live .thinking-label { animation: none; background: none; color: var(--text-muted); }
761
+ }
762
+
763
+ .thinking-header[aria-disabled="true"] { cursor: default; }
764
+ .thinking-header:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
765
+ .thinking-item .thinking-spinner { display: none; }
766
+
731
767
  .thinking-duration {
768
+ flex-shrink: 0;
732
769
  color: var(--text-dimmer);
733
770
  font-size: 12px;
734
771
  }
@@ -753,7 +790,7 @@ pre.mermaid-error {
753
790
  font-size: 13px;
754
791
  line-height: 1.55;
755
792
  color: var(--text-muted);
756
- white-space: pre-wrap;
793
+ white-space: normal;
757
794
  word-break: break-word;
758
795
  max-height: 300px;
759
796
  overflow-y: auto;
@@ -762,6 +799,8 @@ pre.mermaid-error {
762
799
  }
763
800
 
764
801
  .thinking-item.expanded .thinking-content { display: block; }
802
+ .thinking-content > :first-child { margin-top: 0; }
803
+ .thinking-content > :last-child { margin-bottom: 0; }
765
804
 
766
805
  /* ==========================================================================
767
806
  Tool Items
@@ -104,6 +104,49 @@ body.pane-mode #input-area {
104
104
  flex-shrink: 0;
105
105
  }
106
106
 
107
+ .split-pane-header.has-worker-runtime {
108
+ flex-wrap: nowrap;
109
+ }
110
+
111
+ .split-pane-header.has-worker-runtime .split-pane-context {
112
+ order: 1;
113
+ }
114
+
115
+ .split-pane-header.has-worker-runtime .split-pane-close {
116
+ order: 2;
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .split-worker-runtime {
121
+ flex: 0 1 auto;
122
+ min-width: 0;
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ font-size: 11px;
127
+ line-height: 18px;
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .split-worker-runtime-model {
132
+ min-width: 0;
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ white-space: nowrap;
136
+ color: var(--text);
137
+ font-weight: 600;
138
+ }
139
+
140
+ .split-worker-runtime-thinking {
141
+ min-width: 0;
142
+ overflow: hidden;
143
+ text-overflow: ellipsis;
144
+ padding: 0 6px;
145
+ border-radius: 4px;
146
+ background: var(--bg-alt);
147
+ color: var(--text-secondary);
148
+ }
149
+
107
150
  .split-pane-title {
108
151
  flex: 0 1 auto;
109
152
  max-width: 46%;
@@ -192,8 +235,7 @@ body.pane-mode #input-area {
192
235
  @keyframes pair-divider-flow-left { 0%, 100% { transform: translateX(3px); opacity: 0.65; } 50% { transform: translateX(-3px); opacity: 1; } }
193
236
  @keyframes pair-status-pulse { 50% { opacity: 0.55; } }
194
237
 
195
- /* Worker-pane composer notice: shown while this pane's session processes a
196
- delegated turn. Informational only -- the composer stays enabled. */
238
+ /* Worker activity notice shown while this pane processes a delegated task. */
197
239
  .pane-delegation-notice {
198
240
  display: none;
199
241
  align-items: center;
@@ -202,7 +244,7 @@ body.pane-mode #input-area {
202
244
  max-width: var(--content-width);
203
245
  margin: 0 auto;
204
246
  padding: 4px 10px;
205
- border-radius: 8px 8px 0 0;
247
+ border-radius: 8px;
206
248
  background: color-mix(in srgb, var(--accent) 8%, var(--bg));
207
249
  color: var(--text-secondary);
208
250
  font-size: 11px;
@@ -210,13 +252,6 @@ body.pane-mode #input-area {
210
252
  transition: box-shadow 0.2s;
211
253
  }
212
254
  .pane-delegation-notice.visible { display: flex; }
213
- .pane-delegation-notice.visible:has(+ #input-wrapper #input-row:focus-within) {
214
- box-shadow: 0 0 0 1px var(--border);
215
- clip-path: inset(-1px -1px 0 -1px);
216
- }
217
- .pane-delegation-notice.visible + #input-wrapper #input-row {
218
- border-radius: 0 0 8px 8px;
219
- }
220
255
  .pane-delegation-notice::before {
221
256
  content: "";
222
257
  width: 6px;
@@ -141,9 +141,9 @@
141
141
  .worker-proposal-action:not(:disabled):hover { filter: brightness(1.12); }
142
142
  .worker-proposal-action:disabled, .worker-proposal-select:disabled, .worker-proposal-effort-btn:disabled { cursor: default; opacity: 0.58; }
143
143
 
144
- .worker-proposal-error, .worker-proposal-result { margin-top: 10px; padding: 8px 10px; border-radius: 8px; font-size: 10px; line-height: 1.45; }
144
+ .worker-proposal-error { margin-top: 10px; padding: 8px 10px; border-radius: 8px; font-size: 10px; line-height: 1.45; }
145
145
  .worker-proposal-error { color: var(--error); background: var(--error-8); }
146
- .worker-proposal-result { max-height: 96px; overflow: hidden; color: var(--text-muted); background: color-mix(in srgb, var(--success) 7%, transparent); white-space: pre-wrap; }
146
+ .worker-proposal-error.hidden { display: none; }
147
147
  .worker-proposal-card[data-status="starting"] .worker-proposal-status,
148
148
  .worker-proposal-card[data-status="running"] .worker-proposal-status { animation: worker-proposal-pulse 1.6s ease-in-out infinite; }
149
149
  .worker-proposal-card[data-status="completed"] { border-color: color-mix(in srgb, var(--success) 35%, var(--border)); }
@@ -29,6 +29,7 @@ import { handleSessionActionMessage } from './session-actions.js';
29
29
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
30
30
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
31
31
  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';
32
+ import { finishThinkingTurn, resumeThinkingAfterReplay } from './thinking-lifecycle.js';
32
33
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
33
34
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
34
35
  import { beginMarkdownTurn, finishMarkdownTurn, markdownPathFromToolInput } from './markdown-live-edit.js';
@@ -189,6 +190,7 @@ export function processMessage(msg) {
189
190
 
190
191
  case "history_done":
191
192
  store.set({ replayingHistory: false });
193
+ resumeThinkingAfterReplay(store.get('sessionIsProcessing'));
192
194
  // Batched syntax highlight + mermaid pass for the entire replayed
193
195
  // transcript. Per-message highlights are skipped during replay
194
196
  // (see markdown.js) to avoid cascading reflows that the sticky-
@@ -1028,7 +1030,6 @@ export function processMessage(msg) {
1028
1030
  if (typeof msg.text !== "string") break;
1029
1031
  removeMatePreThinking();
1030
1032
  stopThinking();
1031
- resetThinkingGroup();
1032
1033
  setActivity(null);
1033
1034
  appendDelta(msg.text);
1034
1035
  break;
@@ -1228,7 +1229,7 @@ export function processMessage(msg) {
1228
1229
  // were missed by the client).
1229
1230
  removeMatePreThinking();
1230
1231
  setActivity(null);
1231
- stopThinking();
1232
+ finishThinkingTurn();
1232
1233
  markAllToolsDone();
1233
1234
  closeToolGroup();
1234
1235
  clearAllImageGenerationProgress();
@@ -1248,7 +1249,7 @@ export function processMessage(msg) {
1248
1249
  case "done":
1249
1250
  removeMatePreThinking();
1250
1251
  setActivity(null);
1251
- stopThinking();
1252
+ finishThinkingTurn();
1252
1253
  hideWorkerDelegationNotice();
1253
1254
  markAllToolsDone();
1254
1255
  closeToolGroup();
@@ -1271,6 +1272,7 @@ export function processMessage(msg) {
1271
1272
  case "error":
1272
1273
  removeMatePreThinking();
1273
1274
  setActivity(null);
1275
+ finishThinkingTurn();
1274
1276
  addSystemMessage(msg.text, true);
1275
1277
  break;
1276
1278
 
@@ -1322,13 +1324,14 @@ export function processMessage(msg) {
1322
1324
  case "process_conflict":
1323
1325
  removeMatePreThinking();
1324
1326
  setActivity(null);
1327
+ finishThinkingTurn();
1325
1328
  addConflictMessage(msg);
1326
1329
  break;
1327
1330
 
1328
1331
  case "session_writer_conflict":
1329
1332
  removeMatePreThinking();
1330
1333
  setActivity(null);
1331
- stopThinking();
1334
+ finishThinkingTurn();
1332
1335
  markAllToolsDone();
1333
1336
  closeToolGroup();
1334
1337
  addSystemMessage(msg.text || "This session is already open in another process.", true);
@@ -1337,13 +1340,14 @@ export function processMessage(msg) {
1337
1340
  case "context_overflow":
1338
1341
  removeMatePreThinking();
1339
1342
  setActivity(null);
1343
+ finishThinkingTurn();
1340
1344
  addContextOverflowMessage(msg);
1341
1345
  break;
1342
1346
 
1343
1347
  case "auth_required":
1344
1348
  removeMatePreThinking();
1345
1349
  setActivity(null);
1346
- stopThinking();
1350
+ finishThinkingTurn();
1347
1351
  markAllToolsDone();
1348
1352
  closeToolGroup();
1349
1353
  appendDelta((msg.text || "Authentication required.") + "\n");
@@ -3,6 +3,8 @@ import { getWs } from './ws-ref.js';
3
3
  import { VENDOR_AVATARS } from './app-rendering.js';
4
4
  import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
5
5
  import { showToast } from './utils.js';
6
+ import { getCachedSessions } from './sidebar-sessions.js';
7
+ import { appendWorkerRuntime } from './split-worker-runtime.js';
6
8
 
7
9
  var pairDialog = null;
8
10
  // Set when the dialog was opened via "Add Split Worker" on an existing session:
@@ -190,8 +192,7 @@ export function handlePairCreated(msg) {
190
192
 
191
193
  // --- Worker-pane composer notice -------------------------------------
192
194
  // Shown inside a pane iframe while ITS session is processing a delegated
193
- // turn: typing is allowed (queueing is correct behavior), the bar just
194
- // makes it visible that a new message joins the Driver's turn.
195
+ // turn. Identifies the Driver responsible for the current task.
195
196
  var workerNoticeEl = null;
196
197
 
197
198
  export function showWorkerDelegationNotice(driverTitle) {
@@ -205,7 +206,7 @@ export function showWorkerDelegationNotice(driverTitle) {
205
206
  inputArea.insertBefore(workerNoticeEl, inputWrapper);
206
207
  }
207
208
  var who = driverTitle ? "“" + driverTitle + "”" : "the Driver";
208
- workerNoticeEl.textContent = "Following an instruction from " + who + " — messages you send now join the same turn.";
209
+ workerNoticeEl.textContent = "Working on a task from " + who + ".";
209
210
  workerNoticeEl.classList.add("visible");
210
211
  }
211
212
 
@@ -239,8 +240,10 @@ export function syncPairChrome(host, split) {
239
240
  for (var i = 0; i < groups.length; i++) {
240
241
  if (groups[i].id === split.groupId) { group = groups[i]; break; }
241
242
  }
242
- var oldBadges = host.querySelectorAll(".split-pair-role, .split-pair-live, .split-pair-set-driver");
243
+ var oldBadges = host.querySelectorAll(".split-pair-role, .split-pair-live, .split-pair-set-driver, .split-worker-runtime");
243
244
  for (var bi = 0; bi < oldBadges.length; bi++) oldBadges[bi].remove();
245
+ var runtimeHeaders = host.querySelectorAll(".has-worker-runtime");
246
+ for (var ri = 0; ri < runtimeHeaders.length; ri++) runtimeHeaders[ri].classList.remove("has-worker-runtime");
244
247
  host.classList.remove("split-delegating", "split-direction-right", "split-direction-left");
245
248
  if (!group) return;
246
249
  var headers = host.querySelectorAll(".split-pane-header");
@@ -272,6 +275,14 @@ export function syncPairChrome(host, split) {
272
275
  var title = header.querySelector(".split-pane-title");
273
276
  var access = header.querySelector(".split-pane-full-access");
274
277
  header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
278
+ if (!isDriver) {
279
+ var sessions = getCachedSessions() || [];
280
+ var workerSession = null;
281
+ for (var wi = 0; wi < sessions.length; wi++) {
282
+ if (sessions[wi].id === sessionId) { workerSession = sessions[wi]; break; }
283
+ }
284
+ appendWorkerRuntime(header, workerSession);
285
+ }
275
286
  })(split.panes[pi].sessionId, headers[pi]);
276
287
  }
277
288
  var active = (store.get('splitDelegations') || {})[group.id];
@@ -0,0 +1,24 @@
1
+ // Read the Worker's own session metadata, never the active Driver's settings.
2
+ export function workerRuntimeLabels(session) {
3
+ var model = session && typeof session.model === "string" ? session.model : "";
4
+ var effort = session && typeof session.effort === "string" ? session.effort : "";
5
+ var level = effort === "xhigh" ? "X-High" : (effort ? effort.charAt(0).toUpperCase() + effort.slice(1) : "Default");
6
+ return { model: model || "Default model", thinking: "Thinking: " + level };
7
+ }
8
+
9
+ export function appendWorkerRuntime(header, session) {
10
+ var labels = workerRuntimeLabels(session);
11
+ var runtime = document.createElement("div");
12
+ runtime.className = "split-worker-runtime";
13
+ var model = document.createElement("span");
14
+ model.className = "split-worker-runtime-model";
15
+ model.textContent = labels.model;
16
+ model.title = "Model: " + labels.model;
17
+ var thinking = document.createElement("span");
18
+ thinking.className = "split-worker-runtime-thinking";
19
+ thinking.textContent = labels.thinking;
20
+ runtime.appendChild(model);
21
+ runtime.appendChild(thinking);
22
+ header.classList.add("has-worker-runtime");
23
+ header.appendChild(runtime);
24
+ }
@@ -0,0 +1,175 @@
1
+ import { escapeHtml } from './utils.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { store } from './store.js';
4
+ import { addToMessages, scrollToBottom } from './chat-render-runtime.js';
5
+ import { prepareThinkingView, bindThinkingView, updateThinkingView, beginThinkingView, setThinkingViewLive, finishThinkingView } from './thinking-view.js';
6
+
7
+ function getThinkingState() {
8
+ return store.get('thinkingState') || null;
9
+ }
10
+
11
+ function setThinkingState(state) {
12
+ store.set({ thinkingState: state });
13
+ }
14
+
15
+ function updateThinkingState(state, patch) {
16
+ var next = Object.assign({}, state, patch);
17
+ setThinkingState(next);
18
+ return next;
19
+ }
20
+
21
+ function isMateThinking() {
22
+ var target = store.get('dmTargetUser');
23
+ return !!(store.get('dmMode') && target && target.isMate);
24
+ }
25
+
26
+ function maybeScrollToBottom() {
27
+ if (!store.get('replayingHistory')) scrollToBottom();
28
+ }
29
+
30
+ function createThinkingElement() {
31
+ var el = document.createElement("div");
32
+ el.className = "thinking-item";
33
+ if (isMateThinking()) {
34
+ var target = store.get('dmTargetUser');
35
+ var mateName = (target && target.displayName) || "Mate";
36
+ var mateAvatar = document.body.dataset.mateAvatarUrl || "";
37
+ el.classList.add("mate-thinking");
38
+ el.innerHTML =
39
+ '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
40
+ '<div class="dm-bubble-content">' +
41
+ '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
42
+ '<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
43
+ '<div class="thinking-header" style="display:none">' +
44
+ '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
45
+ '<span class="thinking-label">Thinking</span>' +
46
+ '<span class="thinking-duration"></span>' +
47
+ '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
48
+ '</div>' +
49
+ '<div class="thinking-content"></div>' +
50
+ '</div>';
51
+ } else {
52
+ el.innerHTML =
53
+ '<div class="thinking-header">' +
54
+ '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
55
+ '<span class="thinking-label">Thinking</span>' +
56
+ '<span class="thinking-duration"></span>' +
57
+ '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
58
+ '</div>' +
59
+ '<div class="thinking-content"></div>';
60
+ }
61
+ prepareThinkingView(el);
62
+ bindThinkingView(el);
63
+ addToMessages(el);
64
+ return el;
65
+ }
66
+
67
+ function showMateActivity(el, active, hasContent) {
68
+ if (!el.classList.contains("mate-thinking")) return;
69
+ var activity = el.querySelector(".mate-thinking-activity");
70
+ var header = el.querySelector(".thinking-header");
71
+ if (activity) activity.style.display = active ? "" : "none";
72
+ if (header) header.style.display = active || !hasContent ? "none" : "";
73
+ }
74
+
75
+ export function startThinkingSegment() {
76
+ var thinkingState = getThinkingState();
77
+ if (thinkingState && thinkingState.active) return thinkingState.el;
78
+ if (!thinkingState || thinkingState.ended) {
79
+ thinkingState = {
80
+ el: createThinkingElement(),
81
+ detailsText: "",
82
+ segmentText: "",
83
+ latestText: "",
84
+ active: false,
85
+ ended: false,
86
+ startTime: 0,
87
+ totalDuration: 0,
88
+ };
89
+ }
90
+ thinkingState = updateThinkingState(thinkingState, {
91
+ active: true,
92
+ segmentText: "",
93
+ startTime: Date.now(),
94
+ });
95
+ beginThinkingView(thinkingState.el, !store.get('replayingHistory') && !isMateThinking());
96
+ showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
97
+ refreshIcons();
98
+ maybeScrollToBottom();
99
+ return thinkingState.el;
100
+ }
101
+
102
+ export function appendThinkingText(text) {
103
+ var thinkingState = getThinkingState();
104
+ if (!thinkingState || !thinkingState.active || typeof text !== "string" || !text) return;
105
+ var separator = !thinkingState.segmentText && thinkingState.detailsText ? "\n\n" : "";
106
+ var segmentText = thinkingState.segmentText + text;
107
+ thinkingState = updateThinkingState(thinkingState, {
108
+ segmentText: segmentText,
109
+ detailsText: thinkingState.detailsText + separator + text,
110
+ latestText: segmentText.trim() ? segmentText : thinkingState.latestText,
111
+ });
112
+ updateThinkingView(thinkingState.el, thinkingState.segmentText || thinkingState.latestText, thinkingState.detailsText);
113
+ maybeScrollToBottom();
114
+ }
115
+
116
+ export function stopThinkingSegment(duration) {
117
+ var thinkingState = getThinkingState();
118
+ if (!thinkingState || !thinkingState.active) return;
119
+ var seconds = typeof duration === "number" ? duration : (Date.now() - thinkingState.startTime) / 1000;
120
+ thinkingState = updateThinkingState(thinkingState, {
121
+ totalDuration: thinkingState.totalDuration + seconds,
122
+ active: false,
123
+ });
124
+ finishThinkingView(thinkingState.el, thinkingState.latestText, thinkingState.detailsText);
125
+ thinkingState.el.querySelector(".thinking-duration").textContent = " " + thinkingState.totalDuration.toFixed(1) + "s";
126
+ showMateActivity(thinkingState.el, false, !!thinkingState.detailsText.trim());
127
+ }
128
+
129
+ export function finishThinkingTurn() {
130
+ var thinkingState = getThinkingState();
131
+ if (!thinkingState) return;
132
+ stopThinkingSegment();
133
+ thinkingState = getThinkingState();
134
+ thinkingState = updateThinkingState(thinkingState, { ended: true });
135
+ finishThinkingView(thinkingState.el, thinkingState.latestText, thinkingState.detailsText);
136
+ showMateActivity(thinkingState.el, false, !!thinkingState.detailsText.trim());
137
+ }
138
+
139
+ export function resetThinkingTurn() {
140
+ finishThinkingTurn();
141
+ setThinkingState(null);
142
+ }
143
+
144
+ export function clearThinkingState() {
145
+ setThinkingState(null);
146
+ }
147
+
148
+ export function saveThinkingState() {
149
+ return getThinkingState();
150
+ }
151
+
152
+ export function restoreThinkingState(saved) {
153
+ setThinkingState(saved || null);
154
+ }
155
+
156
+ export function resumeThinkingAfterReplay(processing) {
157
+ var thinkingState = getThinkingState();
158
+ if (!thinkingState || !thinkingState.active || !processing) return;
159
+ setThinkingViewLive(thinkingState.el, !isMateThinking());
160
+ showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
161
+ }
162
+
163
+ export function updateThinkingTokenEstimate(estimatedTokens) {
164
+ var thinkingState = getThinkingState();
165
+ if (!thinkingState || !thinkingState.active || !thinkingState.el) return;
166
+ var label = thinkingState.el.querySelector(".thinking-label");
167
+ if (!label) return;
168
+ var count = estimatedTokens || 0;
169
+ var display = count >= 1000 ? ("~" + (Math.round(count / 100) / 10) + "k") : ("~" + count);
170
+ if (thinkingState.latestText.trim()) {
171
+ label.title = label.textContent + " · " + display + " tokens";
172
+ } else {
173
+ label.textContent = "Thinking " + display + " tokens";
174
+ }
175
+ }
@@ -0,0 +1,28 @@
1
+ // Summarize only supplied text; a heading is preferable to a prose excerpt.
2
+ function plainText(text) {
3
+ return text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
4
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
5
+ .replace(/^\s*(?:#{1,6}\s+|>\s*)/gm, "")
6
+ .replace(/[*_`~]/g, "")
7
+ .replace(/\s+/g, " ").trim();
8
+ }
9
+
10
+ export function thinkingSummary(text) {
11
+ if (typeof text !== "string" || !text.trim()) return "Thinking";
12
+ var lines = text.split("\n");
13
+ var heading = "";
14
+ for (var i = 0; i < lines.length; i++) {
15
+ if (/^\s*(?:#{1,6}\s+|\*\*|__)/.test(lines[i])) {
16
+ var candidate = plainText(lines[i]);
17
+ if (candidate) heading = candidate;
18
+ }
19
+ }
20
+ var paragraphs = text.trim().split(/\n\s*\n/);
21
+ var summary = heading || plainText(paragraphs[paragraphs.length - 1]);
22
+ if (!heading) {
23
+ var sentence = summary.match(/^.*?[.!?。!?](?:\s|$)/);
24
+ if (sentence) summary = sentence[0].trim();
25
+ }
26
+ if (!summary) return "Thinking";
27
+ return summary.length > 140 ? summary.slice(0, 139).trimEnd() + "…" : summary;
28
+ }
@@ -0,0 +1,75 @@
1
+ import { thinkingSummary } from './thinking-summary.js';
2
+ import { renderMarkdown } from './markdown.js';
3
+
4
+ export function prepareThinkingView(el) {
5
+ el.hidden = false;
6
+ el.classList.remove("empty", "done", "expanded", "thinking-live");
7
+ var header = el.querySelector(".thinking-header");
8
+ header.setAttribute("role", "button");
9
+ header.setAttribute("aria-expanded", "false");
10
+ header.setAttribute("aria-disabled", "true");
11
+ header.tabIndex = -1;
12
+ header.style.cursor = "";
13
+ el.querySelector(".thinking-chevron").style.display = "";
14
+ el.querySelector(".thinking-label").textContent = "Thinking";
15
+ el.querySelector(".thinking-label").removeAttribute("title");
16
+ el.querySelector(".thinking-duration").textContent = "";
17
+ el.querySelector(".thinking-content").textContent = "";
18
+ }
19
+
20
+ export function bindThinkingView(el) {
21
+ var header = el.querySelector(".thinking-header");
22
+ function toggle() {
23
+ if (header.getAttribute("aria-disabled") === "true") return;
24
+ el.classList.toggle("expanded");
25
+ header.setAttribute("aria-expanded", el.classList.contains("expanded") ? "true" : "false");
26
+ }
27
+ header.addEventListener("click", toggle);
28
+ header.addEventListener("keydown", function (event) {
29
+ if (event.key !== "Enter" && event.key !== " ") return;
30
+ event.preventDefault();
31
+ toggle();
32
+ });
33
+ }
34
+
35
+ export function updateThinkingView(el, summaryText, detailsText) {
36
+ var summary = thinkingSummary(summaryText);
37
+ var label = el.querySelector(".thinking-label");
38
+ label.textContent = summary;
39
+ label.title = summary;
40
+ el.querySelector(".thinking-content").innerHTML = detailsText.trim() ? renderMarkdown(detailsText) : "";
41
+ }
42
+
43
+ export function setThinkingViewLive(el, live) {
44
+ el.classList.toggle("thinking-live", !!live);
45
+ }
46
+
47
+ export function beginThinkingView(el, shimmer) {
48
+ el.hidden = false;
49
+ el.classList.remove("done", "empty", "expanded");
50
+ setThinkingViewLive(el, shimmer);
51
+ var header = el.querySelector(".thinking-header");
52
+ header.setAttribute("aria-expanded", "false");
53
+ header.setAttribute("aria-disabled", "true");
54
+ header.tabIndex = -1;
55
+ header.style.cursor = "";
56
+ el.querySelector(".thinking-chevron").style.display = "";
57
+ }
58
+
59
+ export function finishThinkingView(el, summaryText, detailsText) {
60
+ el.classList.remove("thinking-live");
61
+ el.classList.add("done");
62
+ el.classList.remove("expanded");
63
+ updateThinkingView(el, summaryText, detailsText);
64
+ var hasContent = !!detailsText.trim();
65
+ var header = el.querySelector(".thinking-header");
66
+ header.setAttribute("aria-expanded", "false");
67
+ header.setAttribute("aria-disabled", hasContent ? "false" : "true");
68
+ header.tabIndex = hasContent ? 0 : -1;
69
+ header.style.cursor = hasContent ? "pointer" : "default";
70
+ el.hidden = !hasContent;
71
+ if (!hasContent) {
72
+ el.classList.add("empty");
73
+ el.querySelector(".thinking-chevron").style.display = "none";
74
+ }
75
+ }
@@ -7,6 +7,7 @@ import { mateAvatarUrl } from './avatar.js';
7
7
  import { getChatLayout } from './theme.js';
8
8
  import { store } from './store.js';
9
9
  import { VENDOR_NAMES } from './app-rendering.js';
10
+ import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
10
11
 
11
12
  var ctx;
12
13
 
@@ -46,8 +47,6 @@ var todoMeta = {
46
47
 
47
48
  // --- Tool tracking ---
48
49
  var tools = {};
49
- var currentThinking = null;
50
- var thinkingGroup = null; // { el, count, totalDuration }
51
50
  var pendingPermissions = {};
52
51
 
53
52
  // --- Tool group tracking ---
@@ -1354,12 +1353,7 @@ export function markUserDialogResolved(requestId, behavior) {
1354
1353
  // Annotates the active thinking block's label with a running token estimate.
1355
1354
  // No-op when no thinking block is active.
1356
1355
  export function updateThinkingTokens(estimatedTokens) {
1357
- if (!currentThinking || !currentThinking.el) return;
1358
- var label = currentThinking.el.querySelector(".thinking-label");
1359
- if (!label) return;
1360
- var n = estimatedTokens || 0;
1361
- var disp = n >= 1000 ? ("~" + (Math.round(n / 100) / 10) + "k") : ("~" + n);
1362
- label.textContent = "Thinking " + disp + " tokens";
1356
+ updateThinkingTokenEstimate(estimatedTokens);
1363
1357
  }
1364
1358
 
1365
1359
  // --- Plan mode rendering ---
@@ -1729,128 +1723,23 @@ function updateTodoSticky() {
1729
1723
  // --- Thinking ---
1730
1724
  export function startThinking() {
1731
1725
  ctx.finalizeAssistantBlock();
1732
-
1733
- // Reuse existing thinking group if consecutive
1734
- if (thinkingGroup && thinkingGroup.el.classList.contains("done")) {
1735
- var el = thinkingGroup.el;
1736
- el.classList.remove("done");
1737
- el.querySelector(".thinking-content").textContent = "";
1738
- var reuseLabel = el.querySelector(".thinking-label");
1739
- if (reuseLabel) reuseLabel.textContent = "Thinking";
1740
- // Mate mode: restore dots activity row, hide thinking header
1741
- if (el.classList.contains("mate-thinking")) {
1742
- var actRow = el.querySelector(".mate-thinking-activity");
1743
- if (actRow) {
1744
- actRow.style.display = "";
1745
- }
1746
- var header = el.querySelector(".thinking-header");
1747
- if (header) header.style.display = "none";
1748
- }
1749
- currentThinking = { el: el, fullText: "", startTime: Date.now() };
1750
- refreshIcons();
1751
- maybeScrollToBottom();
1752
- if (!el.classList.contains("mate-thinking")) {
1753
- ctx.setActivity("thinking");
1754
- }
1755
- return;
1756
- }
1757
-
1758
- var el = document.createElement("div");
1759
- el.className = "thinking-item";
1760
-
1761
- if (ctx.isMateDm()) {
1762
- var mateName = ctx.getMateName();
1763
- var mateAvatar = ctx.getMateAvatarUrl();
1764
- el.classList.add("mate-thinking");
1765
- el.innerHTML =
1766
- '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
1767
- '<div class="dm-bubble-content">' +
1768
- '<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
1769
- '<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
1770
- '<div class="thinking-header" style="display:none">' +
1771
- '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1772
- '<span class="thinking-label">Thinking</span>' +
1773
- '<span class="thinking-duration"></span>' +
1774
- '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
1775
- '</div>' +
1776
- '<div class="thinking-content"></div>' +
1777
- '</div>';
1778
- } else {
1779
- el.innerHTML =
1780
- '<div class="thinking-header">' +
1781
- '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
1782
- '<span class="thinking-label">Thinking</span>' +
1783
- '<span class="thinking-duration"></span>' +
1784
- '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
1785
- '</div>' +
1786
- '<div class="thinking-content"></div>';
1787
- }
1788
-
1789
- el.querySelector(".thinking-header").addEventListener("click", function () {
1790
- el.classList.toggle("expanded");
1791
- });
1792
-
1793
- ctx.addToMessages(el);
1794
- refreshIcons();
1795
- maybeScrollToBottom();
1796
- thinkingGroup = { el: el, count: 0, totalDuration: 0 };
1797
- currentThinking = { el: el, fullText: "", startTime: Date.now() };
1726
+ var el = startThinkingSegment();
1798
1727
  if (!ctx.isMateDm()) {
1799
1728
  ctx.setActivity("thinking");
1800
1729
  }
1730
+ return el;
1801
1731
  }
1802
1732
 
1803
1733
  export function appendThinking(text) {
1804
- if (!currentThinking) return;
1805
- currentThinking.fullText += text;
1806
- currentThinking.el.querySelector(".thinking-content").textContent = currentThinking.fullText;
1807
- maybeScrollToBottom();
1734
+ appendThinkingText(text);
1808
1735
  }
1809
1736
 
1810
1737
  export function stopThinking(duration) {
1811
- if (!currentThinking) return;
1812
- var secs = typeof duration === "number" ? duration : (Date.now() - currentThinking.startTime) / 1000;
1813
- currentThinking.el.classList.add("done");
1814
- if (thinkingGroup && thinkingGroup.el === currentThinking.el) {
1815
- thinkingGroup.count++;
1816
- thinkingGroup.totalDuration += secs;
1817
- currentThinking.el.querySelector(".thinking-duration").textContent = " " + thinkingGroup.totalDuration.toFixed(1) + "s";
1818
- } else {
1819
- currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs.toFixed(1) + "s";
1820
- }
1821
- // If no thinking text was streamed (e.g. Codex reasoning items arrive
1822
- // with encrypted/hidden content, or Claude without extended-thinking),
1823
- // the expand affordance is misleading because there's nothing inside.
1824
- // Strip the chevron and the click handler so the header reads as a
1825
- // plain label.
1826
- var hasContent = !!(currentThinking.fullText && currentThinking.fullText.length > 0);
1827
- if (!hasContent) {
1828
- currentThinking.el.classList.add("empty");
1829
- var chev = currentThinking.el.querySelector(".thinking-chevron");
1830
- if (chev) chev.style.display = "none";
1831
- var hdr = currentThinking.el.querySelector(".thinking-header");
1832
- if (hdr) {
1833
- hdr.style.cursor = "default";
1834
- // Replace click listener by cloning the node (cheapest way to strip listeners).
1835
- var clone = hdr.cloneNode(true);
1836
- hdr.parentNode.replaceChild(clone, hdr);
1837
- }
1838
- }
1839
- // In mate mode: hide sparkle activity, show compact thinking header.
1840
- if (currentThinking.el.classList.contains("mate-thinking")) {
1841
- var actRow = currentThinking.el.querySelector(".mate-thinking-activity");
1842
- if (actRow) actRow.style.display = "none";
1843
- var header = currentThinking.el.querySelector(".thinking-header");
1844
- if (header) {
1845
- header.style.display = "";
1846
- header.style.cursor = hasContent ? "pointer" : "default";
1847
- }
1848
- }
1849
- currentThinking = null;
1738
+ stopThinkingSegment(duration);
1850
1739
  }
1851
1740
 
1852
1741
  export function resetThinkingGroup() {
1853
- thinkingGroup = null;
1742
+ resetThinkingTurn();
1854
1743
  }
1855
1744
 
1856
1745
  // --- Tool items ---
@@ -2521,7 +2410,7 @@ export function getHiddenResultTools() { return HIDDEN_RESULT_TOOLS; }
2521
2410
  export function saveToolState() {
2522
2411
  return {
2523
2412
  tools: tools,
2524
- currentThinking: currentThinking,
2413
+ thinkingState: saveThinkingState(),
2525
2414
  todoWidgetEl: todoWidgetEl,
2526
2415
  todoMeta: todoMeta,
2527
2416
  inPlanMode: inPlanMode,
@@ -2536,7 +2425,7 @@ export function saveToolState() {
2536
2425
 
2537
2426
  export function restoreToolState(saved) {
2538
2427
  tools = saved.tools;
2539
- currentThinking = saved.currentThinking;
2428
+ restoreThinkingState(saved.thinkingState);
2540
2429
  todoWidgetEl = saved.todoWidgetEl;
2541
2430
  todoMeta = saved.todoMeta || normalizeTodoMeta();
2542
2431
  inPlanMode = saved.inPlanMode;
@@ -2553,8 +2442,7 @@ export function restoreToolState(saved) {
2553
2442
 
2554
2443
  export function resetToolState() {
2555
2444
  tools = {};
2556
- currentThinking = null;
2557
- thinkingGroup = null;
2445
+ clearThinkingState();
2558
2446
  inPlanMode = false;
2559
2447
  planContent = null;
2560
2448
  currentPlanCardEl = null;
@@ -128,11 +128,6 @@ function applyState(card, msg) {
128
128
  error.textContent = msg.error || "";
129
129
  error.classList.toggle("hidden", !msg.error);
130
130
  }
131
- var preview = card.querySelector(".worker-proposal-result");
132
- if (preview && msg.resultPreview) {
133
- preview.textContent = msg.resultPreview;
134
- preview.classList.remove("hidden");
135
- }
136
131
  setControlsDisabled(card, status !== "pending");
137
132
  var actions = card.querySelector(".worker-proposal-actions");
138
133
  if (actions) actions.classList.toggle("hidden", autoAccepted);
@@ -248,9 +243,6 @@ export function renderWorkerProposal(msg) {
248
243
  var error = document.createElement("div");
249
244
  error.className = "worker-proposal-error hidden";
250
245
  card.appendChild(error);
251
- var result = document.createElement("div");
252
- result.className = "worker-proposal-result hidden";
253
- card.appendChild(result);
254
246
 
255
247
  var actions = document.createElement("div");
256
248
  actions.className = "worker-proposal-actions";
package/lib/sdk-bridge.js CHANGED
@@ -2121,6 +2121,7 @@ function createSDKBridge(opts) {
2121
2121
  // No active query — just store the model for next startQuery
2122
2122
  session.model = model;
2123
2123
  sm.saveSessionFile(session);
2124
+ sm.broadcastSessionList();
2124
2125
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2125
2126
  // Confirm against the session vendor so a non-default vendor does not
2126
2127
  // receive model metadata labeled as the project's default adapter.
@@ -2134,6 +2135,7 @@ function createSDKBridge(opts) {
2134
2135
  await session.queryInstance.setModel(model);
2135
2136
  session.model = model;
2136
2137
  sm.saveSessionFile(session);
2138
+ sm.broadcastSessionList();
2137
2139
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2138
2140
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
2139
2141
  sm.defaultModelByVendor[sessionVendor] = model;
@@ -2156,6 +2158,7 @@ function createSDKBridge(opts) {
2156
2158
  sm.currentEffortByVendor[effortVendor] = clamped;
2157
2159
  session.effort = clamped;
2158
2160
  sm.saveSessionFile(session);
2161
+ sm.broadcastSessionList();
2159
2162
  sm.currentEffort = clamped;
2160
2163
  if (!session.queryInstance) {
2161
2164
  return clamped;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.0-beta.24",
3
+ "version": "4.0.0-beta.25",
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",