clay-server 3.4.0-beta.14 → 3.4.0-beta.15

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.
@@ -282,6 +282,7 @@ function attachConnection(ctx) {
282
282
  if (active.isProcessing) {
283
283
  sendTo(ws, { type: "status", status: "processing" });
284
284
  }
285
+ sendTo(ws, { type: "active_background_tasks", tasks: active.activeBackgroundTasks || [] });
285
286
  var pendingIds = Object.keys(active.pendingPermissions);
286
287
  for (var pi = 0; pi < pendingIds.length; pi++) {
287
288
  var p = active.pendingPermissions[pendingIds[pi]];
package/lib/public/app.js CHANGED
@@ -65,6 +65,7 @@ import { initMisc, flushPendingExtMessages, showImageModal as _miscShowImageModa
65
65
  import { initSkillInstall, requireSkills as _siRequireSkills, requireClayMateInterview as _siRequireClayMateInterview, handleSkillInstallWs as _siHandleSkillInstallWs } from './modules/app-skills-install.js';
66
66
  import { initDebateUi, showDebateConcludeConfirm as _debShowDebateConcludeConfirm, exitDebateConcludeMode as _debExitDebateConcludeMode, handleDebateConcludeSend as _debHandleDebateConcludeSend, showDebateEndedMode as _debShowDebateEndedMode, exitDebateEndedMode as _debExitDebateEndedMode, showDebateUserFloor as _debShowDebateUserFloor, exitDebateFloorMode as _debExitDebateFloorMode, handleDebateFloorSend as _debHandleDebateFloorSend, renderDebateUserFloorDone as _debRenderDebateUserFloorDone, showDebateSticky as _debShowDebateSticky, showDebateBottomBar as _debShowDebateBottomBar, removeDebateBottomBar as _debRemoveDebateBottomBar, sendDebateStickyComment as _debSendDebateStickyComment, updateDebateRound as _debUpdateDebateRound } from './modules/app-debate-ui.js';
67
67
  import { initLoopUi, updateLoopInputVisibility as _loopUpdateLoopInputVisibility, updateLoopButton as _loopUpdateLoopButton, showLoopBanner as _loopShowLoopBanner, updateLoopBanner as _loopUpdateLoopBanner, updateRalphBars as _loopUpdateRalphBars, showRalphCraftingBar as _loopShowRalphCraftingBar, showRalphApprovalBar as _loopShowRalphApprovalBar, updateRalphApprovalStatus as _loopUpdateRalphApprovalStatus, openRalphPreviewModal as _loopOpenRalphPreviewModal, showExecModal as _loopShowExecModal, closeExecModal as _loopCloseExecModal, updateExecModalStatus as _loopUpdateExecModalStatus } from './modules/app-loop-ui.js';
68
+ import { initBackgroundTasksUi } from './modules/background-tasks-ui.js';
68
69
  import { initLoopWizard, openRalphWizard as _loopOpenRalphWizard, closeRalphWizard as _loopCloseRalphWizard, getWizardSource as _loopGetWizardSource } from './modules/app-loop-wizard.js';
69
70
  import { initAppNotifications, handleNotificationsState as _notifHandleState, handleNotificationCreated as _notifHandleCreated, handleNotificationDismissed as _notifHandleDismissed, handleNotificationDismissedAll as _notifHandleDismissedAll } from './modules/app-notifications.js';
70
71
  import { initWhatsNew, handleWhatsNewState as _wnHandleState, handleWhatsNewSeenResult as _wnHandleSeenResult } from './modules/whats-new.js';
@@ -296,6 +297,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
296
297
  slashCommands: [],
297
298
  processing: false,
298
299
  activeSessionId: null,
300
+ activeBackgroundTasks: [],
299
301
  sessionVendorBound: false,
300
302
  cliSessionId: null,
301
303
  isHeadlessMode: false,
@@ -649,6 +651,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
649
651
  initDebateUi();
650
652
 
651
653
  initLoopUi();
654
+ initBackgroundTasksUi();
652
655
  initLoopWizard();
653
656
 
654
657
  // --- Notifications module ---
@@ -667,6 +667,50 @@
667
667
  position: relative;
668
668
  }
669
669
 
670
+ .background-tasks-bar {
671
+ max-width: var(--content-width);
672
+ margin: 0 auto 8px;
673
+ border: 1px solid var(--border);
674
+ border-radius: 8px;
675
+ background: var(--input-bg);
676
+ overflow: hidden;
677
+ }
678
+
679
+ .background-tasks-toggle {
680
+ width: 100%;
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 7px;
684
+ padding: 8px 10px;
685
+ border: 0;
686
+ background: transparent;
687
+ color: var(--text-secondary);
688
+ cursor: pointer;
689
+ font: inherit;
690
+ text-align: left;
691
+ }
692
+
693
+ .background-tasks-chip, .session-background-task-count {
694
+ display: inline-flex;
695
+ align-items: center;
696
+ justify-content: center;
697
+ min-width: 16px;
698
+ height: 16px;
699
+ padding: 0 4px;
700
+ border-radius: 8px;
701
+ background: var(--accent);
702
+ color: var(--bg);
703
+ font-size: 10px;
704
+ font-weight: 700;
705
+ }
706
+
707
+ .background-tasks-list { padding: 0 10px 8px; }
708
+ .background-task-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border-subtle); }
709
+ .background-task-description { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
710
+ .background-task-type { padding: 2px 6px; border-radius: 5px; background: var(--code-bg); color: var(--text-muted); font-size: 11px; }
711
+ .background-task-stop { border: 1px solid var(--border); border-radius: 5px; padding: 3px 7px; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: 12px; }
712
+ .background-task-stop:hover { color: var(--text); border-color: var(--text-muted); }
713
+
670
714
  #input-row {
671
715
  display: flex;
672
716
  flex-direction: column;
@@ -1128,6 +1128,10 @@ export function processMessage(msg) {
1128
1128
  updateSubagentTaskStatus(msg.parentToolId, msg.patch);
1129
1129
  break;
1130
1130
 
1131
+ case "active_background_tasks":
1132
+ store.set({ activeBackgroundTasks: Array.isArray(msg.tasks) ? msg.tasks : [] });
1133
+ break;
1134
+
1131
1135
  case "result":
1132
1136
  // Result marks turn end. Drop pre-thinking even if no delta/tool
1133
1137
  // event ever arrived (e.g. tool-only turn whose progress signals
@@ -0,0 +1,55 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { escapeHtml } from './utils.js';
4
+
5
+ var expanded = false;
6
+
7
+ export function initBackgroundTasksUi() {
8
+ store.subscribe(function(state, prev) {
9
+ if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
10
+ });
11
+ renderBackgroundTasks();
12
+ }
13
+
14
+ function renderBackgroundTasks() {
15
+ var tasks = store.get('activeBackgroundTasks') || [];
16
+ var existing = document.getElementById('background-tasks-bar');
17
+ if (tasks.length === 0) {
18
+ if (existing) existing.remove();
19
+ expanded = false;
20
+ return;
21
+ }
22
+ var inputArea = document.getElementById('input-area');
23
+ if (!inputArea || !inputArea.parentNode) return;
24
+ var bar = existing || document.createElement('div');
25
+ bar.id = 'background-tasks-bar';
26
+ bar.className = 'background-tasks-bar';
27
+ var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
28
+ var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '">' +
29
+ '<span aria-hidden="true">⏳</span><span class="background-tasks-chip">' + tasks.length + '</span>' +
30
+ '<span>' + tasks.length + ' ' + taskLabel + '</span></button>';
31
+ if (expanded) {
32
+ html += '<div class="background-tasks-list">';
33
+ for (var i = 0; i < tasks.length; i++) {
34
+ var task = tasks[i];
35
+ html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
36
+ escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
37
+ escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
38
+ escapeHtml(task.task_id || '') + '">Stop</button></div>';
39
+ }
40
+ html += '</div>';
41
+ }
42
+ bar.innerHTML = html;
43
+ if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
44
+ bar.querySelector('.background-tasks-toggle').addEventListener('click', function() {
45
+ expanded = !expanded;
46
+ renderBackgroundTasks();
47
+ });
48
+ var stopButtons = bar.querySelectorAll('.background-task-stop');
49
+ for (var j = 0; j < stopButtons.length; j++) {
50
+ stopButtons[j].addEventListener('click', function() {
51
+ var ws = getWs();
52
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
53
+ });
54
+ }
55
+ }
@@ -977,6 +977,9 @@ function renderLoopChild(s) {
977
977
  if (s.isProcessing) {
978
978
  textHtml += '<span class="session-processing"></span>';
979
979
  }
980
+ if (s.backgroundTaskCount) {
981
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
982
+ }
980
983
  if (s.loop) {
981
984
  var isRalphChild = s.loop.source === "ralph";
982
985
  var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
@@ -1096,6 +1099,9 @@ function renderLoopGroup(loopId, children, groupKey) {
1096
1099
  if (anyProcessing) {
1097
1100
  textHtml += '<span class="session-processing"></span>';
1098
1101
  }
1102
+ var groupBackgroundTaskCount = 0;
1103
+ for (var bi = 0; bi < children.length; bi++) groupBackgroundTaskCount += children[bi].backgroundTaskCount || 0;
1104
+ if (groupBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + groupBackgroundTaskCount + '</span>';
1099
1105
  var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
1100
1106
  var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
1101
1107
  textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
@@ -1205,6 +1211,9 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1205
1211
  if (anyProcessing) {
1206
1212
  textHtml += '<span class="session-processing"></span>';
1207
1213
  }
1214
+ var runBackgroundTaskCount = 0;
1215
+ for (var bi = 0; bi < sessions.length; bi++) runBackgroundTaskCount += sessions[bi].backgroundTaskCount || 0;
1216
+ if (runBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + runBackgroundTaskCount + '</span>';
1208
1217
  textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
1209
1218
  textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
1210
1219
  textSpan.innerHTML = textHtml;
@@ -1254,6 +1263,9 @@ function renderSessionItem(s) {
1254
1263
  if (s.isProcessing) {
1255
1264
  textHtml += '<span class="session-processing"></span>';
1256
1265
  }
1266
+ if (s.backgroundTaskCount) {
1267
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
1268
+ }
1257
1269
  if (s.loop && s.loop.source === "debate") {
1258
1270
  textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
1259
1271
  }
@@ -1322,6 +1334,8 @@ function renderSplitGroupItem(group, members) {
1322
1334
  }
1323
1335
  html += "</span>";
1324
1336
  if (members[0].isProcessing || members[1].isProcessing) html += '<span class="session-processing"></span>';
1337
+ var splitBackgroundTaskCount = (members[0].backgroundTaskCount || 0) + (members[1].backgroundTaskCount || 0);
1338
+ if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
1325
1339
  html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
1326
1340
  text.innerHTML = html;
1327
1341
  el.appendChild(text);
@@ -175,6 +175,10 @@ function attachMessageProcessor(ctx) {
175
175
 
176
176
  // Cache slash_commands and model from CLI init message
177
177
  if (parsed.yokeType === "init") {
178
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
179
+ session.activeBackgroundTasks = [];
180
+ sendToSession(session, { type: "active_background_tasks", tasks: [] });
181
+ if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
178
182
  var fsSkills = discoverSkillDirs();
179
183
  sm.skillNames = mergeSkills(parsed.skills, fsSkills);
180
184
  if (parsed.slashCommands) {
@@ -612,6 +616,15 @@ function attachMessageProcessor(ctx) {
612
616
  });
613
617
  }
614
618
 
619
+ } else if (parsed.yokeType === "background_tasks_changed") {
620
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
621
+ var activeBackgroundTasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
622
+ session.activeBackgroundTasks = activeBackgroundTasks;
623
+ sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
624
+ if (previousBackgroundTaskCount !== activeBackgroundTasks.length) {
625
+ sm.broadcastSessionList();
626
+ }
627
+
615
628
  } else if (parsed.yokeType === "tool_progress") {
616
629
  // Sub-agent tool_progress: forward as activity update
617
630
  var parentId = parsed.parentToolId;
package/lib/sessions.js CHANGED
@@ -495,6 +495,7 @@ function createSessionManager(opts) {
495
495
  title: s.title || "New Session",
496
496
  active: isActive,
497
497
  isProcessing: s.isProcessing,
498
+ backgroundTaskCount: (s.activeBackgroundTasks || []).length,
498
499
  lastActivity: s.lastActivity || s.createdAt || 0,
499
500
  loop: loop,
500
501
  spawn: s.spawn || null,
@@ -717,6 +718,7 @@ function createSessionManager(opts) {
717
718
  if (session.isProcessing) {
718
719
  _send({ type: "status", status: "processing" });
719
720
  }
721
+ _send({ type: "active_background_tasks", tasks: session.activeBackgroundTasks || [] });
720
722
 
721
723
  // Re-send any pending permission requests
722
724
  var pendingIds = Object.keys(session.pendingPermissions);
package/lib/ws-schema.js CHANGED
@@ -114,6 +114,7 @@ var schema = {
114
114
  "subagent_done": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Subagent task completed" },
115
115
  "task_started": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Background task started" },
116
116
  "task_progress": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Background task progress update" },
117
+ "active_background_tasks": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Current background tasks for the active session (full replacement)" },
117
118
  "markdown_edit_present": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Open a user-requested Markdown edit with a before snapshot" },
118
119
 
119
120
  // -----------------------------------------------------------------------
@@ -197,6 +197,26 @@ function flattenEvent(raw) {
197
197
  base.summary = raw.summary || null;
198
198
  return base;
199
199
  }
200
+ if (raw.subtype === "background_tasks_changed") {
201
+ base.yokeType = "background_tasks_changed";
202
+ base.tasks = normalizeBackgroundTasks(raw.tasks);
203
+ return base;
204
+ }
205
+ if (raw.subtype === "task_updated") {
206
+ base.yokeType = "task_updated";
207
+ base.task_id = raw.task_id;
208
+ base.patch = raw.patch || {};
209
+ return base;
210
+ }
211
+ if (raw.subtype === "task_notification") {
212
+ base.yokeType = "task_notification";
213
+ base.parentToolId = raw.parent_tool_use_id;
214
+ base.taskId = raw.task_id;
215
+ base.status = raw.status || "completed";
216
+ base.summary = raw.summary || "";
217
+ base.usage = raw.usage || null;
218
+ return base;
219
+ }
200
220
  // Model refusal: the model declined and the CLI either fell back to
201
221
  // another model or ended the turn. Translate to a vendor-neutral
202
222
  // yokeType so the relay never sees SDK subtype strings.
@@ -343,6 +363,20 @@ function flattenEvent(raw) {
343
363
  return base;
344
364
  }
345
365
 
366
+ function normalizeBackgroundTasks(tasks) {
367
+ if (!Array.isArray(tasks)) return [];
368
+ return tasks.map(function(task) {
369
+ var nativeType = task && task.task_type;
370
+ var taskType = nativeType === "local_bash" ? "shell"
371
+ : nativeType === "local_agent" ? "agent" : "other";
372
+ return {
373
+ task_id: task && task.task_id,
374
+ task_type: taskType,
375
+ description: (task && task.description) || "",
376
+ };
377
+ });
378
+ }
379
+
346
380
  // --- QueryHandle ---
347
381
  // Wraps a raw SDK query object with the YOKE QueryHandle interface.
348
382
  // Events are flattened via flattenEvent before yielding.
@@ -9,6 +9,17 @@
9
9
  // 2. QueryHandle (returned by adapter.createQuery)
10
10
 
11
11
  var TOOL_POLICIES = ["ask", "allow-all"];
12
+ var BACKGROUND_TASK_TYPES = ["shell", "agent", "monitor", "other"];
13
+
14
+ /*
15
+ * Optional adapter event contract:
16
+ * { yokeType: "background_tasks_changed", tasks: [{ task_id, task_type, description }] }
17
+ *
18
+ * Each event replaces the complete live set. An adapter that emits this event
19
+ * must reset it to an empty set when its CLI process restarts. task_type uses
20
+ * BACKGROUND_TASK_TYPES; adapters without a level signal may synthesize one,
21
+ * and adapters that emit nothing simply have no background-task indicator.
22
+ */
12
23
 
13
24
  /**
14
25
  * Validate that an adapter object implements all required methods.
@@ -93,6 +104,7 @@ function validateQueryHandle(handle) {
93
104
 
94
105
  module.exports = {
95
106
  TOOL_POLICIES: TOOL_POLICIES,
107
+ BACKGROUND_TASK_TYPES: BACKGROUND_TASK_TYPES,
96
108
  ADAPTER_METHODS: ADAPTER_METHODS,
97
109
  QUERY_HANDLE_METHODS: QUERY_HANDLE_METHODS,
98
110
  validateAdapter: validateAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.14",
3
+ "version": "3.4.0-beta.15",
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",