clay-server 2.36.1-beta.5 → 2.36.2-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.
@@ -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
  }
@@ -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/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-beta.5",
3
+ "version": "2.36.2-beta.1",
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",