clay-server 2.36.1 → 2.36.2-beta.2

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.
@@ -330,10 +330,11 @@ function attachSessions(ctx) {
330
330
  var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
331
331
  var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
332
332
  var found = false;
333
+ var _curLc = sm.currentModel.toLowerCase();
333
334
  for (var tvi = 0; tvi < tvModels.length; tvi++) {
334
335
  var tvEntry = tvModels[tvi];
335
336
  var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
336
- if (tvVal === sm.currentModel) { found = true; break; }
337
+ if (tvVal === sm.currentModel || (tvVal && (tvVal.toLowerCase().indexOf(_curLc) !== -1 || _curLc.indexOf(tvVal.toLowerCase()) !== -1))) { found = true; break; }
337
338
  }
338
339
  if (tvModels.length > 0 && !found) {
339
340
  sm.currentModel = "";
@@ -1346,6 +1346,38 @@ pre.mermaid-error {
1346
1346
  padding: 0 20px;
1347
1347
  }
1348
1348
 
1349
+ /* Dead-session compaction: when a session is resumed without a live SDK
1350
+ process and the todo widget still has pending/in_progress items, the
1351
+ item list is hidden so the widget doesn't anchor visual position
1352
+ mid-page. Click the header to toggle the .todo-widget-dead-expanded
1353
+ override class which restores the full view. */
1354
+ .todo-widget-dead-compact .todo-header {
1355
+ cursor: pointer;
1356
+ border-radius: 12px;
1357
+ }
1358
+ .todo-widget-dead-compact .todo-header::after {
1359
+ content: "paused";
1360
+ font-size: 10px;
1361
+ color: var(--text-muted);
1362
+ background: rgba(var(--overlay-rgb), 0.06);
1363
+ padding: 2px 6px;
1364
+ border-radius: 4px;
1365
+ margin-left: 6px;
1366
+ letter-spacing: 0.04em;
1367
+ text-transform: uppercase;
1368
+ }
1369
+ .todo-widget-dead-compact .todo-progress,
1370
+ .todo-widget-dead-compact .todo-items {
1371
+ display: none;
1372
+ }
1373
+ .todo-widget-dead-compact.todo-widget-dead-expanded .todo-header {
1374
+ border-radius: 12px 12px 0 0;
1375
+ }
1376
+ .todo-widget-dead-compact.todo-widget-dead-expanded .todo-progress,
1377
+ .todo-widget-dead-compact.todo-widget-dead-expanded .todo-items {
1378
+ display: block;
1379
+ }
1380
+
1349
1381
  .todo-header {
1350
1382
  display: flex;
1351
1383
  align-items: center;
@@ -8,7 +8,7 @@ import { getWs } from './ws-ref.js';
8
8
  // --- Leaf module imports ---
9
9
  import { showToast } from './utils.js';
10
10
  import { refreshIcons, iconHtml } from './icons.js';
11
- import { renderMarkdown } from './markdown.js';
11
+ import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
12
12
  import { updatePageTitle } from './sidebar.js';
13
13
  import { renderSessionList, updateSessionPresence, populateCliSessionList, handleSearchResults, updateSessionBadge } from './sidebar-sessions.js';
14
14
  import { updateDmBadge, renderSidebarPresence, setMentionActive, renderUserStrip } from './sidebar-mates.js';
@@ -20,7 +20,7 @@ import { renderMemoryList } from './mate-memory.js';
20
20
  import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
21
21
  import { handleFindInSessionResults } from './session-search.js';
22
22
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
23
- import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
23
+ 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 } from './tools.js';
24
24
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
25
25
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch } from './filebrowser.js';
26
26
  import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
@@ -149,6 +149,20 @@ export function processMessage(msg) {
149
149
 
150
150
  case "history_done":
151
151
  store.set({ replayingHistory: false });
152
+ // Batched syntax highlight + mermaid pass for the entire replayed
153
+ // transcript. Per-message highlights are skipped during replay
154
+ // (see markdown.js) to avoid cascading reflows that the sticky-
155
+ // bottom observer chases for several seconds on long sessions.
156
+ if (messagesEl) {
157
+ highlightCodeBlocks(messagesEl);
158
+ renderMermaidBlocks(messagesEl);
159
+ }
160
+ // Compact dead-session todo widgets (unfinished items will never
161
+ // resolve — the agent isn't coming back) so they don't anchor
162
+ // visual position mid-page on resume.
163
+ if (!store.get('sessionIsProcessing')) {
164
+ applyDeadSessionTodoCompaction();
165
+ }
152
166
  // Hide vendor toggle if session has history (vendor already locked)
153
167
  var _hTotal = store.get('historyTotal') || 0;
154
168
  var _vtw2 = document.getElementById("vendor-toggle-wrap");
@@ -558,7 +572,7 @@ export function processMessage(msg) {
558
572
  } else if (_prevSid) {
559
573
  delete store.get('sessionDrafts')[_prevSid];
560
574
  }
561
- store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, vendorCapabilities: msg.capabilities || {} });
575
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing });
562
576
  if (msg.vendor) {
563
577
  if (!store.get('vendorSelectionLocked') || msg.hasHistory) {
564
578
  store.set({ currentVendor: msg.vendor });
@@ -715,6 +729,10 @@ export function processMessage(msg) {
715
729
  case "status":
716
730
  if (msg.status === "processing") {
717
731
  setStatus("processing");
732
+ // Session became live — undo any dead-session todo compaction
733
+ // applied at history_done time.
734
+ store.set({ sessionIsProcessing: true });
735
+ applyDeadSessionTodoCompaction();
718
736
  if (!(store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) && !store.get('matePreThinkingEl')) {
719
737
  setActivity("thinking");
720
738
  }
@@ -749,8 +749,9 @@ export function toggleStatusPanel() {
749
749
  // --- Context panel ---
750
750
 
751
751
  export function resolveContextWindow(model, sdkValue) {
752
- if (sdkValue) return sdkValue;
753
752
  var lc = (model || "").toLowerCase();
753
+ if (lc.includes("[1m]")) return 1000000;
754
+ if (sdkValue) return sdkValue;
754
755
  for (var key in KNOWN_CONTEXT_WINDOWS) {
755
756
  if (lc.includes(key)) return KNOWN_CONTEXT_WINDOWS[key];
756
757
  }
@@ -851,8 +852,11 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
851
852
  if (models.length > 0) {
852
853
  var m = models[0];
853
854
  var mu = modelUsage[m];
854
- contextData.model = m;
855
- contextData.contextWindow = resolveContextWindow(m, mu.contextWindow);
855
+ // Prefer the user-configured model name over the API-reported one
856
+ // (e.g. CLI reports "claude-sonnet-4-6" even when running as opus[1m])
857
+ var displayModel = store.get('currentModel') || m;
858
+ contextData.model = displayModel;
859
+ contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
856
860
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
857
861
  }
858
862
  }
@@ -1,6 +1,7 @@
1
1
  import { copyToClipboard, escapeHtml } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { getMermaidThemeVars } from './theme.js';
4
+ import { store } from './store.js';
4
5
 
5
6
  // Initialize markdown parser
6
7
  marked.use({ gfm: true, breaks: false });
@@ -57,6 +58,13 @@ export function parseEmojis(el) {
57
58
  var langAliases = { jsonl: "json", dotenv: "bash" };
58
59
 
59
60
  export function highlightCodeBlocks(el) {
61
+ // Defer all syntax highlighting + copy-button injection while replaying
62
+ // history. Doing this per-message during replay triggers a long tail of
63
+ // sequential reflows that the sticky-bottom scroll observer chases for
64
+ // 4-8s. After history_done, app-messages.js calls this once on the full
65
+ // messages container, so unhighlighted blocks (selector :not(.hljs))
66
+ // are picked up in a single pass.
67
+ if (store.get('replayingHistory')) return;
60
68
  el.querySelectorAll("pre code:not(.hljs):not(.language-mermaid)").forEach(function (block) {
61
69
  var cls = Array.from(block.classList).find(function (c) { return c.startsWith("language-"); });
62
70
  if (cls) {
@@ -93,6 +101,9 @@ export function highlightCodeBlocks(el) {
93
101
  }
94
102
 
95
103
  export function renderMermaidBlocks(el) {
104
+ // Same rationale as highlightCodeBlocks: defer mermaid renders until
105
+ // history_done so they don't cause cascading layout shifts during replay.
106
+ if (store.get('replayingHistory')) return;
96
107
  var blocks = el.querySelectorAll("pre code.language-mermaid");
97
108
  blocks.forEach(function (codeEl) {
98
109
  var pre = codeEl.parentElement;
@@ -30,6 +30,11 @@ var todoItems = [];
30
30
  var todoWidgetEl = null;
31
31
  var todoWidgetVisible = true; // whether in-chat widget is in viewport
32
32
  var todoObserver = null;
33
+ // When a session is resumed without a live SDK process and still has
34
+ // pending/in_progress items, the widget is rendered in compact mode
35
+ // (header + count only) so it doesn't anchor visual position mid-page
36
+ // or extend the sticky-bottom settle window with a tall element.
37
+ var todoDeadCompact = false;
33
38
  var todoMeta = {
34
39
  variant: "tasks",
35
40
  title: "Tasks",
@@ -1289,7 +1294,12 @@ export function handleTodoWrite(input) {
1289
1294
  activeForm: t.activeForm || "",
1290
1295
  };
1291
1296
  });
1297
+ // A fresh TodoWrite during replay is just historical state. After replay
1298
+ // ends, applyDeadSessionTodoCompaction (called from history_done) decides
1299
+ // whether to compact. During live operation, sessionIsProcessing is true
1300
+ // and applyDeadSessionTodoCompaction is a no-op for compaction.
1292
1301
  renderTodoWidget();
1302
+ applyDeadSessionTodoCompaction();
1293
1303
  }
1294
1304
 
1295
1305
  export function handleTaskCreate(input) {
@@ -1359,7 +1369,9 @@ function renderTodoWidget() {
1359
1369
  todoWidgetEl = document.createElement("div");
1360
1370
  todoWidgetEl.className = "todo-widget";
1361
1371
  }
1362
- todoWidgetEl.className = "todo-widget" + (todoMeta.variant === "plan" ? " todo-widget-plan" : "");
1372
+ todoWidgetEl.className = "todo-widget"
1373
+ + (todoMeta.variant === "plan" ? " todo-widget-plan" : "")
1374
+ + (todoDeadCompact ? " todo-widget-dead-compact" : "");
1363
1375
 
1364
1376
  var completed = 0;
1365
1377
  for (var i = 0; i < todoItems.length; i++) {
@@ -1395,12 +1407,38 @@ function renderTodoWidget() {
1395
1407
  if (isNew) {
1396
1408
  ctx.addToMessages(todoWidgetEl);
1397
1409
  setupTodoObserver();
1410
+ // Click-to-expand for compact mode. Toggling the override class lets
1411
+ // the user inspect items without permanently un-compacting the widget.
1412
+ todoWidgetEl.addEventListener("click", function (e) {
1413
+ if (!todoWidgetEl.classList.contains("todo-widget-dead-compact")) return;
1414
+ // Only expand on header click, not on items inside an already-expanded view
1415
+ var header = e.target.closest(".todo-header");
1416
+ if (!header) return;
1417
+ todoWidgetEl.classList.toggle("todo-widget-dead-expanded");
1418
+ });
1398
1419
  }
1399
1420
  updateTodoSticky();
1400
1421
  refreshIcons();
1401
1422
  maybeScrollToBottom();
1402
1423
  }
1403
1424
 
1425
+ export function applyDeadSessionTodoCompaction() {
1426
+ // Called after history_done and on status changes. Decides whether the
1427
+ // current session is "dead" (resumed, no live SDK process) and whether
1428
+ // the todo widget has unfinished items that will never resolve.
1429
+ var isLive = !!store.get('sessionIsProcessing');
1430
+ var hasUnfinished = false;
1431
+ for (var i = 0; i < todoItems.length; i++) {
1432
+ var s = todoItems[i].status;
1433
+ if (s === "in_progress" || s === "pending") { hasUnfinished = true; break; }
1434
+ }
1435
+ todoDeadCompact = !isLive && hasUnfinished;
1436
+ if (todoWidgetEl) {
1437
+ todoWidgetEl.classList.toggle("todo-widget-dead-compact", todoDeadCompact);
1438
+ if (!todoDeadCompact) todoWidgetEl.classList.remove("todo-widget-dead-expanded");
1439
+ }
1440
+ }
1441
+
1404
1442
  function setupTodoObserver() {
1405
1443
  if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
1406
1444
  if (!todoWidgetEl) return;
package/lib/sdk-bridge.js CHANGED
@@ -198,6 +198,24 @@ function createSDKBridge(opts) {
198
198
  return false;
199
199
  }
200
200
 
201
+ // Resolve a shorthand model name (e.g. "opus[1m]") to its full ID
202
+ // in the vendor model list (e.g. "claude-opus-4.6[1m]").
203
+ function resolveModelInList(list, modelId) {
204
+ if (!list || !modelId) return null;
205
+ var lc = modelId.toLowerCase();
206
+ for (var mi = 0; mi < list.length; mi++) {
207
+ var val = modelEntryValue(list[mi]);
208
+ if (val === modelId) return val;
209
+ }
210
+ for (var mi = 0; mi < list.length; mi++) {
211
+ var val = modelEntryValue(list[mi]);
212
+ if (!val || val === "default") continue;
213
+ var vlc = val.toLowerCase();
214
+ if (vlc.indexOf(lc) !== -1 || lc.indexOf(vlc) !== -1) return val;
215
+ }
216
+ return null;
217
+ }
218
+
201
219
  function sendModelInfoForVendor(vendor, model) {
202
220
  send({
203
221
  type: "model_info",
@@ -1234,12 +1252,13 @@ function createSDKBridge(opts) {
1234
1252
  // Claude would reject the unknown model. We validate against the
1235
1253
  // session vendor's model list regardless of which vendor happens to be
1236
1254
  // the project's default adapter.
1237
- var queryModel = ls.model || sm.currentModel || undefined;
1255
+ var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || sm.currentModel || undefined;
1238
1256
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1239
1257
  if (sessionVendor) {
1240
1258
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
1241
1259
  if (vendorModels.length > 0 && queryModel && !modelListContains(vendorModels, queryModel)) {
1242
- queryModel = modelEntryValue(vendorModels[0]);
1260
+ var resolved = resolveModelInList(vendorModels, queryModel);
1261
+ queryModel = resolved || modelEntryValue(vendorModels[0]);
1243
1262
  }
1244
1263
  }
1245
1264
  // Guard against anything upstream having set queryModel to an object
package/lib/sessions.js CHANGED
@@ -345,7 +345,10 @@ function createSessionManager(opts) {
345
345
  return session;
346
346
  }
347
347
 
348
- var HISTORY_PAGE_SIZE = 200;
348
+ // Initial replay payload size. Lowered from 200 to reduce client-side
349
+ // layout work on resume — older items are loaded progressively on
350
+ // scroll-up via the existing pagination path.
351
+ var HISTORY_PAGE_SIZE = 100;
349
352
 
350
353
  function findTurnBoundary(history, targetIndex) {
351
354
  for (var i = targetIndex; i >= 0; i--) {
@@ -420,7 +423,7 @@ function createSessionManager(opts) {
420
423
  var _capsByVendor = capabilitiesByVendor || {};
421
424
  var _sessionVendor = session.vendor || defaultVendor || "claude";
422
425
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
423
- _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps });
426
+ _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing });
424
427
  // Send vendor-specific slash commands
425
428
  var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
426
429
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.36.1",
3
+ "version": "2.36.2-beta.2",
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",