clay-server 5.3.0-beta.6 → 5.3.0

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.
Files changed (111) hide show
  1. package/lib/project-admin-overview.js +22 -11
  2. package/lib/project-background-tasks.js +76 -0
  3. package/lib/project-connection.js +1 -1
  4. package/lib/project-sessions.js +12 -5
  5. package/lib/project-split-views.js +12 -3
  6. package/lib/project-view-store.js +7 -5
  7. package/lib/project-worker-conversation.js +5 -0
  8. package/lib/public/app.js +7 -0
  9. package/lib/public/css/contextual-clay.css +2 -1
  10. package/lib/public/css/mate-conversation.css +1 -1
  11. package/lib/public/css/pending-message-queue.css +24 -19
  12. package/lib/public/css/project-admin-overview.css +42 -13
  13. package/lib/public/css/shared-browser.css +31 -8
  14. package/lib/public/css/split-view-layout.css +92 -7
  15. package/lib/public/css/tool-pane.css +4 -7
  16. package/lib/public/index.html +5 -5
  17. package/lib/public/modules/agent-message-bubbles.js +96 -0
  18. package/lib/public/modules/app-header.js +6 -0
  19. package/lib/public/modules/app-messages.js +26 -9
  20. package/lib/public/modules/background-task-previews.js +8 -0
  21. package/lib/public/modules/background-task-viewer.js +97 -0
  22. package/lib/public/modules/background-tasks-ui.js +34 -125
  23. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  24. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  25. package/lib/public/modules/pane-activation.js +5 -3
  26. package/lib/public/modules/pane-header-tooltips.js +32 -0
  27. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  28. package/lib/public/modules/pending-message-queue.js +3 -33
  29. package/lib/public/modules/permission-control.js +6 -5
  30. package/lib/public/modules/project-admin-overview.js +125 -10
  31. package/lib/public/modules/project-grouping.js +9 -0
  32. package/lib/public/modules/server-settings-controls.js +33 -4
  33. package/lib/public/modules/session-folder-toolbar.js +1 -1
  34. package/lib/public/modules/session-tool-item.js +86 -0
  35. package/lib/public/modules/settings-select.js +2 -1
  36. package/lib/public/modules/shared-browser-tabs.js +37 -21
  37. package/lib/public/modules/sidebar-projects.js +1 -10
  38. package/lib/public/modules/split-session-empty-pane.js +47 -0
  39. package/lib/public/modules/split-view-create-menu.js +74 -65
  40. package/lib/public/modules/split-view-layout-scope.js +4 -0
  41. package/lib/public/modules/split-view-picker-actions.js +71 -0
  42. package/lib/public/modules/split-view-picker-render.js +81 -0
  43. package/lib/public/modules/split-view-presentation-renderer.js +8 -8
  44. package/lib/public/modules/split-view-worker-activity.js +2 -2
  45. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  46. package/lib/public/modules/split-view-workers.js +6 -4
  47. package/lib/public/modules/terminal-viewport.js +14 -0
  48. package/lib/public/modules/terminal.js +3 -4
  49. package/lib/public/modules/theme-palette.js +177 -0
  50. package/lib/public/modules/theme.js +1 -174
  51. package/lib/public/modules/tool-pane-chrome.js +7 -1
  52. package/lib/public/modules/tools.js +5 -89
  53. package/lib/public/modules/tooltip.js +32 -6
  54. package/lib/public/modules/wireframe-workbench.js +2 -2
  55. package/lib/public/modules/worker-message-previews.js +39 -0
  56. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  57. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  58. package/lib/public/pane-assets/{chunk-BMHTBRMO.js → chunk-7OVNC5HT.js} +1 -1
  59. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  60. package/lib/public/pane-assets/{chunk-2AIWCI6Y.js → chunk-EMLN2Y7L.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-VKYBAVWT.js → chunk-ENPAHXLB.js} +1 -1
  62. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  63. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  64. package/lib/public/pane-assets/{chunk-6NYCNWVF.js → chunk-FQTNR4ZS.js} +1 -1
  65. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  66. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  67. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  68. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  69. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  70. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  71. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  72. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  73. package/lib/public/pane-assets/{chunk-PV4PCH2V.js → chunk-SJOT74VW.js} +1 -1
  74. package/lib/public/pane-assets/{chunk-LDIZLKB2.js → chunk-SJTGHIN5.js} +1 -1
  75. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  76. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  77. package/lib/public/pane-assets/{chunk-VNAV23CM.js → chunk-UPXU2IPC.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-TMGSDFJI.js → chunk-VEBYD7TL.js} +1 -1
  79. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  80. package/lib/public/pane-assets/manifest.json +411 -399
  81. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  82. package/lib/sdk-message-processor.js +5 -2
  83. package/lib/server-storage.js +27 -3
  84. package/lib/server.js +4 -2
  85. package/lib/sessions.js +1 -1
  86. package/lib/storage-usage.js +29 -7
  87. package/lib/terminal-manager.js +2 -2
  88. package/lib/worker-message-previews.js +50 -0
  89. package/lib/ws-schema.js +1 -1
  90. package/lib/yoke/adapters/claude-worker.js +8 -0
  91. package/lib/yoke/adapters/claude.js +17 -2
  92. package/lib/yoke/claude-task-reader.js +94 -0
  93. package/lib/yoke/interface.js +1 -0
  94. package/package.json +1 -1
  95. package/lib/public/pane-assets/app-7UZ727NA.js +0 -1
  96. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +0 -1
  97. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +0 -1
  98. package/lib/public/pane-assets/chunk-6XK2OATZ.js +0 -1
  99. package/lib/public/pane-assets/chunk-BKIDR2ME.js +0 -1
  100. package/lib/public/pane-assets/chunk-GEFCFZEN.js +0 -1
  101. package/lib/public/pane-assets/chunk-GN2NQEVC.js +0 -2
  102. package/lib/public/pane-assets/chunk-IMLMBPT4.js +0 -166
  103. package/lib/public/pane-assets/chunk-K5S6AILM.js +0 -1
  104. package/lib/public/pane-assets/chunk-OWNNLZ36.js +0 -1
  105. package/lib/public/pane-assets/chunk-P2C3LOWL.js +0 -3
  106. package/lib/public/pane-assets/chunk-R4LMPTKU.js +0 -1
  107. package/lib/public/pane-assets/chunk-VWTHIQCE.js +0 -1
  108. package/lib/public/pane-assets/chunk-WM3K5AFH.js +0 -1
  109. package/lib/public/pane-assets/chunk-XOB3O327.js +0 -1
  110. package/lib/public/pane-assets/chunk-XOOUCFYT.js +0 -1
  111. package/lib/public/pane-assets/style-ZM7OOU3M.css +0 -1
@@ -229,7 +229,8 @@ function attachMessageProcessor(ctx) {
229
229
  // only clears ambient watchers must not rebroadcast the session list.
230
230
  var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(session.activeBackgroundTasks);
231
231
  session.activeBackgroundTasks = [];
232
- sendToSession(session, { type: "active_background_tasks", tasks: [] });
232
+ session.backgroundTaskCatalog = null;
233
+ sendToSession(session, { type: "active_background_tasks", tasks: [], catalog: [] });
233
234
  if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
234
235
  var fsSkills = discoverSkillDirs(getSessionSkillOptions(session));
235
236
  session.skillNames = Array.from(mergeSkills(parsed.skills, fsSkills));
@@ -463,6 +464,7 @@ function attachMessageProcessor(ctx) {
463
464
 
464
465
  } else if (parsed.yokeType === "subagent_message") {
465
466
  // Sub-agent messages: extract tool_use blocks for activity display
467
+ require("./project-background-tasks").observeMessage(session, parsed, sendToSession);
466
468
  processSubagentMessage(session, parsed);
467
469
 
468
470
  } else if (parsed.yokeType === "message") {
@@ -864,7 +866,8 @@ function attachMessageProcessor(ctx) {
864
866
  Array.isArray(parsed.tasks) ? parsed.tasks : []
865
867
  );
866
868
  session.activeBackgroundTasks = activeBackgroundTasks;
867
- sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
869
+ var taskCatalog = require("./project-background-tasks").remember(session, activeBackgroundTasks);
870
+ sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks, catalog: taskCatalog });
868
871
  if (previousBackgroundTaskCount !== backgroundTaskTiming.countUserTasks(activeBackgroundTasks)) {
869
872
  sm.broadcastSessionList();
870
873
  }
@@ -6,6 +6,21 @@ function attachStorage(ctx) {
6
6
  var progress = null;
7
7
  var expires = 0;
8
8
  var failed = false;
9
+ var scanIdentity = null;
10
+ function projectIdentity() {
11
+ return JSON.stringify((ctx.getProjects ? ctx.getProjects() : []).map(function (project) { return [project.slug, project.path, project.projectOwnerId, project.isMate]; }));
12
+ }
13
+ function targetProject(slug) {
14
+ return (ctx.getProjects ? ctx.getProjects() : []).find(function (project) { return project.slug === slug && !project.isMate && !/^mate-/.test(slug); });
15
+ }
16
+ function usageReply(req, res, result, slug) {
17
+ if (!authorized(req, res)) return;
18
+ if (!slug) { reply(res, result.scanning ? 202 : 200, result); return; }
19
+ if (!targetProject(slug)) { reply(res, 404, { error: 'Project is no longer registered.' }); return; }
20
+ if (scanIdentity !== projectIdentity()) { reply(res, 409, { error: 'Project registration changed. Refresh usage.' }); return; }
21
+ var usage = (result.projects || []).find(function (project) { return project.slug === slug; });
22
+ reply(res, result.scanning ? 202 : 200, { slug: slug, scanning: !!result.scanning, partial: !!result.partial, checkedAt: result.checkedAt || null, usage: usage || null });
23
+ }
9
24
  function reply(res, status, data) {
10
25
  res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
11
26
  res.end(JSON.stringify(data));
@@ -19,14 +34,23 @@ function attachStorage(ctx) {
19
34
  return true;
20
35
  }
21
36
  function handleRequest(req, res, url) {
22
- if (url.split('?')[0] !== '/api/server/storage') return false;
37
+ var pathname = url.split('?')[0];
38
+ var match = /^\/api\/server\/storage\/project\/([^/]+)$/.exec(pathname);
39
+ if (pathname !== '/api/server/storage' && !match) return false;
40
+ var slug = null;
23
41
  var refresh = new URL(url, 'http://localhost').searchParams.get('refresh') === '1';
24
42
  if (!authorized(req, res)) return true;
25
43
  if (req.method !== 'GET') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
44
+ if (match) {
45
+ try { slug = decodeURIComponent(match[1]); } catch (error) { reply(res, 400, { error: 'Invalid project.' }); return true; }
46
+ if (!targetProject(slug)) { reply(res, 404, { error: 'Project not found.' }); return true; }
47
+ }
48
+ if (!pending && scanIdentity !== projectIdentity()) { cached = null; failed = false; }
26
49
  if (failed && Date.now() < expires) { reply(res, 500, { error: 'Disk usage could not be measured. Try again in a few seconds.' }); return true; }
27
- if (!pending && !refresh && cached && Date.now() < expires) { reply(res, 200, cached); return true; }
50
+ if (!pending && !refresh && cached && Date.now() < expires) { usageReply(req, res, cached, slug); return true; }
28
51
  if (!pending) {
29
52
  progress = null; failed = false;
53
+ scanIdentity = projectIdentity();
30
54
  var users = ctx.users.getAllUsers();
31
55
  var projects = ctx.getProjects ? ctx.getProjects() : [];
32
56
  if (!ctx.users.isMultiUser() && users.length <= 1) {
@@ -45,7 +69,7 @@ function attachStorage(ctx) {
45
69
  timer = setTimeout(function () { resolve(progress || { scanning: true }); }, 750);
46
70
  })]).then(function (result) {
47
71
  clearTimeout(timer);
48
- if (!res.destroyed && authorized(req, res)) reply(res, result.scanning ? 202 : 200, result);
72
+ if (!res.destroyed) usageReply(req, res, result, slug);
49
73
  }).catch(function () {
50
74
  clearTimeout(timer);
51
75
  if (!res.destroyed) reply(res, 500, { error: 'Disk usage could not be measured. Try refreshing.' });
package/lib/server.js CHANGED
@@ -519,7 +519,7 @@ function createServer(opts) {
519
519
  } : null;
520
520
  var payload = JSON.stringify(seed).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
521
521
  var script = '<script>window.__clayAppearanceSeed=' + payload + ';</script>';
522
- return html.replace('<script src="appearance-bootstrap.js"></script>', script + '<script src="appearance-bootstrap.js"></script>');
522
+ return html.replace(/<script src="\/?appearance-bootstrap.js"><\/script>/, function (tag) { return script + tag; });
523
523
  }
524
524
 
525
525
  function sendAppShell(req, res, slug) {
@@ -1103,8 +1103,10 @@ function createServer(opts) {
1103
1103
  if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
1104
1104
  var overview = httpUser && administrativeMetadata(httpUser.id, slug);
1105
1105
  if (overview && req.method === "GET" && (projectUrlForAuth === "/" || projectUrlForAuth === "/index.html")) {
1106
+ var overviewAppearance = null;
1107
+ try { overviewAppearance = settings.getInitialAppearance(req); } catch (error) {}
1106
1108
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
1107
- res.end(projectAdminOverview.page(overview, getVisibleProjects(httpUser.id)));
1109
+ res.end(injectAppearanceSeed(projectAdminOverview.page(overview, getVisibleProjects(httpUser.id)), overviewAppearance));
1108
1110
  return;
1109
1111
  }
1110
1112
  res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
package/lib/sessions.js CHANGED
@@ -886,7 +886,7 @@ function createSessionManager(opts) {
886
886
  if (session.isProcessing) {
887
887
  _send({ type: "status", status: "processing" });
888
888
  }
889
- _send({ type: "active_background_tasks", tasks: session.activeBackgroundTasks || [] });
889
+ _send({ type: "active_background_tasks", tasks: session.activeBackgroundTasks || [], catalog: session.backgroundTaskCatalog && session.backgroundTaskCatalog.tasks || [] });
890
890
 
891
891
  // Re-send any pending permission requests
892
892
  var pendingIds = Object.keys(session.pendingPermissions);
@@ -39,10 +39,26 @@ async function scanStorage(options) {
39
39
  if (identity && !owners.get(identity.uid)) rows[index].mapped = false;
40
40
  });
41
41
  var projectRoots = new Map();
42
+ var projectUsage = [];
43
+ var usageRoots = new Map();
42
44
  var projectErrors = 0;
43
45
  for (var project of projects) {
46
+ var usage = { slug: project.slug, available: false, bytes: 0, files: 0, capacityBytes: null, availableBytes: null };
47
+ if (!project.isMate && project.slug) projectUsage.push(usage);
44
48
  try {
45
49
  var projectPath = await fs.realpath(project.path);
50
+ usage.available = true;
51
+ if (usageRoots.has(projectPath)) {
52
+ var priorUsage = usageRoots.get(projectPath);
53
+ if (priorUsage) priorUsage.available = false;
54
+ usage.available = false;
55
+ usageRoots.set(projectPath, null);
56
+ } else usageRoots.set(projectPath, project.isMate ? null : usage);
57
+ try {
58
+ var volume = await fs.statfs(projectPath);
59
+ usage.capacityBytes = volume.blocks * volume.bsize;
60
+ usage.availableBytes = volume.bavail * volume.bsize;
61
+ } catch (error) { /* Volume capacity can be unavailable independently of allocation. */ }
46
62
  var row = rows.find(function (item) { return item.id === project.projectOwnerId; }) || null;
47
63
  if (row) row.projects++;
48
64
  if (projectRoots.has(projectPath) && projectRoots.get(projectPath) !== row) row = null;
@@ -50,21 +66,20 @@ async function scanStorage(options) {
50
66
  } catch (error) { projectErrors++; }
51
67
  }
52
68
  var orderedProjects = Array.from(projectRoots.keys()).sort(function (a, b) { return b.length - a.length; });
53
- function projectOwner(filePath) {
54
- var root = orderedProjects.find(function (candidate) { return filePath === candidate || filePath.indexOf(candidate + path.sep) === 0; });
55
- return root ? projectRoots.get(root) : null;
69
+ function projectRoot(filePath) {
70
+ return orderedProjects.find(function (candidate) { return filePath === candidate || filePath.indexOf(candidate + path.sep) === 0; });
56
71
  }
57
72
  var roots = orderedProjects.concat(options.projectPaths || []);
58
73
  identities.forEach(function (identity) { if (identity) roots.push(identity.home); });
59
74
  roots.push(options.configDir);
60
- var report = { attribution: projectMode ? "project" : "filesystem", users: rows, totalBytes: 0, totalFiles: 0, unattributedBytes: 0, unattributedFiles: 0,
75
+ var report = { attribution: projectMode ? "project" : "filesystem", users: rows, projects: projectUsage, totalBytes: 0, totalFiles: 0, unattributedBytes: 0, unattributedFiles: 0,
61
76
  partial: false, errors: projectErrors + users.filter(function (user, index) { return !projectMode && options.platform === 'linux' && user.linuxUser && !identities[index]; }).length, skippedLinks: 0, skippedMounts: 0, scannedEntries: 0, roots: 0, platform: options.platform };
62
77
  var lastProgress = 0;
63
78
  function progress() {
64
79
  if (!options.onProgress || Date.now() - lastProgress < 500) return;
65
80
  lastProgress = Date.now();
66
81
  options.onProgress(Object.assign({}, report, { scanning: true, checkedAt: new Date().toISOString(),
67
- users: rows.map(function (row) { return Object.assign({}, row); }) }));
82
+ users: rows.map(function (row) { return Object.assign({}, row); }), projects: projectUsage.map(function (row) { return Object.assign({}, row); }) }));
68
83
  }
69
84
  progress();
70
85
  var seen = new Map();
@@ -118,9 +133,15 @@ async function scanStorage(options) {
118
133
  if (stat.isSymbolicLink()) { report.skippedLinks++; return; }
119
134
  if (stat.dev !== device) { report.skippedMounts++; return; }
120
135
  var key = stat.dev + ':' + stat.ino;
121
- var owner = projectMode ? projectOwner(filePath) : owners.get(stat.uid);
136
+ var usageRoot = projectRoot(filePath);
137
+ var owner = projectMode ? projectRoots.get(usageRoot) : owners.get(stat.uid);
138
+ var usage = usageRoot && usageRoots.get(usageRoot);
122
139
  if (seen.has(key)) {
123
140
  var previous = seen.get(key);
141
+ if (stat.isFile() && previous.project && usage && previous.project !== usage) {
142
+ previous.project.bytes -= previous.bytes; previous.project.files--;
143
+ previous.project = null;
144
+ }
124
145
  if (projectMode && stat.isFile() && previous.owner && previous.owner !== owner) {
125
146
  previous.owner.bytes -= previous.bytes; previous.owner.files--;
126
147
  report.unattributedBytes += previous.bytes; report.unattributedFiles++;
@@ -132,7 +153,8 @@ async function scanStorage(options) {
132
153
  if (!stat.isDirectory() && !stat.isFile()) return;
133
154
  var bytes = typeof stat.blocks === 'number' ? stat.blocks * 512 : stat.size;
134
155
  // Only directories and multiply-linked files need identity deduplication.
135
- if (stat.isDirectory() || stat.nlink > 1) seen.set(key, { owner: owner, bytes: bytes });
156
+ if (stat.isDirectory() || stat.nlink > 1) seen.set(key, { owner: owner, project: usage, bytes: bytes });
157
+ if (usage) { usage.bytes += bytes; if (stat.isFile()) usage.files++; }
136
158
  report.totalBytes += bytes;
137
159
  if (stat.isFile()) report.totalFiles++;
138
160
  if (owner) { owner.bytes += bytes; if (stat.isFile()) owner.files++; }
@@ -136,9 +136,9 @@ function createTerminalManager(opts) {
136
136
  sendTo(ws, { type: "term_output", id: id, data: replay });
137
137
  }
138
138
 
139
- // Send current terminal dimensions so the client renders at the correct size
139
+ // This snapshot may arrive after the attaching client has fitted a new viewport.
140
140
  if (!alreadySubscribed && session.cols && session.rows) {
141
- sendTo(ws, { type: "term_resized", id: id, cols: session.cols, rows: session.rows });
141
+ sendTo(ws, { type: "term_resized", id: id, cols: session.cols, rows: session.rows, initial: true });
142
142
  }
143
143
 
144
144
  // If already exited, notify
@@ -0,0 +1,50 @@
1
+ // Bounded, exact-owner message previews use the Worker reader's existing authority.
2
+ function latest(history) {
3
+ var found = null;
4
+ for (var i = history.length - 1, stop = Math.max(0, history.length - 1000); i >= stop; i--) {
5
+ var entry = history[i];
6
+ if (entry && (entry.type === 'delta' || entry.type === 'assistant_message') && typeof entry.text === 'string' && entry.text.trim()) {
7
+ var index = i, text = entry.text;
8
+ while (index > stop && history[index - 1].type === 'delta' && entry.type === 'delta' ) { index--; text = (history[index].text + text).slice(0, 1200); }
9
+ found = { id: String(index), text: text.slice(0, 1200), timestamp: history[index]._ts || null }; break;
10
+ }
11
+ }
12
+ return found;
13
+ }
14
+ function attach(ctx) {
15
+ var subscriptions = new WeakMap();
16
+ function release(ws) {
17
+ var old = subscriptions.get(ws); if (!old) return;
18
+ subscriptions.delete(ws); old.entries.forEach(function (entry) { clearTimeout(entry.timer); if (entry.release) entry.release(); });
19
+ if (ws.removeListener) ws.removeListener('close', old.close);
20
+ }
21
+ function handleMessage(ws, msg) {
22
+ if (msg.type !== 'split_worker_preview_request') return false;
23
+ release(ws);
24
+ if (typeof msg.requestId !== 'string' || msg.requestId.length > 160 || !Array.isArray(msg.targets) || msg.targets.length > 32) return true;
25
+ var state = { entries: [], close: function () { release(ws); } }; subscriptions.set(ws, state);
26
+ msg.targets.forEach(function (target) {
27
+ var pair = ctx.resolve(ws, target); if (!pair || state.entries.some(function (entry) { return entry.worker === pair.worker; })) return;
28
+ var entry = { worker: pair.worker, timer: null, last: null };
29
+ function send(live) {
30
+ var current = ctx.resolve(ws, target);
31
+ if (subscriptions.get(ws) !== state || ws.readyState !== 1 || !current || current.driver !== pair.driver || current.worker !== pair.worker) { if (entry.release) entry.release(); return; }
32
+ var preview = latest(pair.worker.history || []); if (!preview) return;
33
+ preview.vendor = pair.worker.vendor || null;
34
+ if (entry.last === JSON.stringify(preview)) return;
35
+ entry.last = JSON.stringify(preview);
36
+ ctx.send(ws, { type: 'split_worker_message_preview', requestId: msg.requestId, parentSessionOriginId: target.parentSessionOriginId,
37
+ workerSessionOriginId: target.workerSessionOriginId, generation: target.generation, live: live, preview: preview });
38
+ }
39
+ entry.release = ctx.sm.subscribeSession(pair.worker.localId, function (event) {
40
+ if (!event || ['delta', 'assistant_message'].indexOf(event.type) < 0 || entry.timer) return;
41
+ entry.timer = setTimeout(function () { entry.timer = null; send(true); }, 60);
42
+ });
43
+ state.entries.push(entry); send(false);
44
+ });
45
+ if (ws.once) ws.once('close', state.close);
46
+ return true;
47
+ }
48
+ return { handleMessage: handleMessage };
49
+ }
50
+ module.exports = { attach: attach, latest: latest };
package/lib/ws-schema.js CHANGED
@@ -24,7 +24,7 @@ var schema = {
24
24
  "switch_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Switch the active session by local ID" },
25
25
  "resume_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Spawn the claude --resume PTY for a TUI session shown read-only (lazy resume)" },
26
26
  "suspend_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Close a live TUI session's PTY now but keep it resumable (explicit Close)" },
27
- "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session (opts: vendor, mode, sessionVisibility, dangerouslySkipPermissions)" },
27
+ "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a blank session; correlated ordinary-project requests may use activate:false to preserve the current View selection" },
28
28
  "new_session_result": { direction: "s2c", handler: "lib/public/modules/scheduled-tasks.js", description: "Correlated result for a requested new session" },
29
29
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
30
30
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
@@ -97,6 +97,8 @@ function applyWorkerClaudeBinary(options) {
97
97
  // --- State ---
98
98
  var sdkModule = null;
99
99
  var queryInstance = null;
100
+ var taskReader = null;
101
+ var taskReaderModule = require("../claude-task-reader");
100
102
  var messageQueue = null;
101
103
  var abortController = null;
102
104
  var pendingPermissions = {}; // requestId -> resolve
@@ -284,6 +286,9 @@ function handleMessage(msg) {
284
286
  case "set_permission_mode":
285
287
  handleSetPermissionMode(msg);
286
288
  break;
289
+ case "read_background_task":
290
+ taskReaderModule.respond(taskReader, msg, sendToDaemon);
291
+ break;
287
292
  case "stop_task":
288
293
  handleStopTask(msg);
289
294
  break;
@@ -598,6 +603,8 @@ async function handleQueryStart(msg) {
598
603
  prompt: messageQueue,
599
604
  options: options,
600
605
  });
606
+ if (taskReader) taskReader.close();
607
+ taskReader = taskReaderModule.createReader(getSDK, function () { return queryInstance; }, options.cwd);
601
608
  perf("query instance created");
602
609
  } catch (e) {
603
610
  sendToDaemon({ type: "query_error", error: "Failed to create query: " + (e.message || e), exitCode: null, stderr: null });
@@ -618,6 +625,7 @@ async function handleQueryStart(msg) {
618
625
  var firstText = true;
619
626
  var eventCounts = {};
620
627
  for await (var event of queryInstance) {
628
+ taskReader.observe(event);
621
629
  var etype = (event && event.type || "?");
622
630
  var esubtype = (event && event.subtype || "");
623
631
  eventCounts[etype] = (eventCounts[etype] || 0) + 1;
@@ -18,6 +18,7 @@ var claudeUserInput = require("../claude-user-input");
18
18
  var claudeCorrelation = require("../claude-message-correlation");
19
19
  var claudeContextUsage = require("../claude-context-usage");
20
20
  var claudeModelSwitch = require("../claude-model-switch");
21
+ var taskReaderModule = require("../claude-task-reader");
21
22
 
22
23
  // SDK-specific knowledge stays in the adapter (not the relay): the host dialog
23
24
  // kinds this client can render, declared to the SDK when an onUserDialog
@@ -580,6 +581,8 @@ function normalizeBackgroundTasks(tasks) {
580
581
  task_id: task && task.task_id,
581
582
  task_type: taskType,
582
583
  description: (task && task.description) || "",
584
+ run_id: task && task.run_id || null,
585
+ parent_task_id: task && task.parent_task_id || null,
583
586
  // Housekeeping the CLI does not surface as user work (skip_transcript
584
587
  // tasks, auto-started live-update watchers). Clay keeps these out of its
585
588
  // activity indicators and lists them as a subdued secondary group.
@@ -597,7 +600,8 @@ function normalizeBackgroundTasks(tasks) {
597
600
  // --- QueryHandle ---
598
601
  // Wraps a raw SDK query object with the YOKE QueryHandle interface.
599
602
  // Events are flattened via flattenEvent before yielding.
600
- function createQueryHandle(rawQuery, messageQueue, abortController) {
603
+ function createQueryHandle(rawQuery, messageQueue, abortController, cwd) {
604
+ var taskReader = taskReaderModule.createReader(loadSDK, function () { return closed ? null : rawQuery; }, cwd);
601
605
  var correlation = claudeCorrelation.createCorrelation();
602
606
  var closed = false;
603
607
  var removeAbort = function () {};
@@ -615,6 +619,7 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
615
619
  return rawIter.next().then(function(result) {
616
620
  if (result.done) { closed = true; removeAbort(); resolveStopped(); }
617
621
  if (result.done) return result;
622
+ taskReader.observe(result.value);
618
623
  return { value: correlation.result(result.value, flattenEvent(result.value)), done: false };
619
624
  }, function(err) {
620
625
  closed = true;
@@ -676,6 +681,8 @@ function createQueryHandle(rawQuery, messageQueue, abortController) {
676
681
  return Promise.resolve();
677
682
  },
678
683
 
684
+ readBackgroundTask: taskReader.read,
685
+
679
686
  stopTask: function(taskId) {
680
687
  if (rawQuery && typeof rawQuery.stopTask === "function") {
681
688
  return rawQuery.stopTask(taskId);
@@ -1107,6 +1114,7 @@ function serializeWorkerValue(value, seen) {
1107
1114
  function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool, onUserDialog, onUserInputRequest, onModelSwitch, preToolUsePolicy) {
1108
1115
  var interactionAbort = new AbortController();
1109
1116
  var contextRequests = claudeContextUsage.createRequests(worker);
1117
+ var taskRequests = taskReaderModule.createRequests(worker);
1110
1118
  var correlation = claudeCorrelation.createCorrelation();
1111
1119
  // Async iterable state
1112
1120
  var iterQueue = [];
@@ -1153,6 +1161,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1153
1161
  function endIter() {
1154
1162
  interactionAbort.abort();
1155
1163
  contextRequests.close();
1164
+ taskRequests.close();
1156
1165
  if (iterEnded) return;
1157
1166
  iterEnded = true;
1158
1167
  if (iterWaiting) {
@@ -1165,6 +1174,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1165
1174
  function errorIter(err) {
1166
1175
  interactionAbort.abort();
1167
1176
  contextRequests.close();
1177
+ taskRequests.close();
1168
1178
  if (iterEnded) return;
1169
1179
  iterEnded = true;
1170
1180
  iterError = err;
@@ -1292,6 +1302,9 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1292
1302
  case "model_switched":
1293
1303
  if (onModelSwitch) onModelSwitch(msg.change);
1294
1304
  break;
1305
+ case "background_task_response":
1306
+ taskRequests.receive(msg);
1307
+ break;
1295
1308
  case "context_usage_response":
1296
1309
  contextRequests.receive(msg);
1297
1310
  break;
@@ -1458,6 +1471,8 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
1458
1471
  });
1459
1472
  },
1460
1473
 
1474
+ readBackgroundTask: function(input) { return iterEnded ? Promise.reject(new Error("Task connection closed.")) : taskRequests.request(input); },
1475
+
1461
1476
  stopTask: function(taskId) {
1462
1477
  worker.send({ type: "stop_task", taskId: taskId });
1463
1478
  return Promise.resolve();
@@ -1821,7 +1836,7 @@ function createClaudeAdapter(opts) {
1821
1836
 
1822
1837
  claudeModelSwitch.attach(sdkOptions, queryOpts.onModelSwitch);
1823
1838
  var rawQuery = sdk.query({ prompt: mq, options: sdkOptions });
1824
- return createQueryHandle(rawQuery, mq, ac);
1839
+ return createQueryHandle(rawQuery, mq, ac, sdkOptions.cwd || _cwd);
1825
1840
  },
1826
1841
 
1827
1842
  createOneShotQuery: async function(queryOpts) {
@@ -0,0 +1,94 @@
1
+ // Native task inspection stays inside the process and OS identity owning the query.
2
+ var crypto = require('crypto');
3
+ function validId(value) { return typeof value === 'string' && /^[a-zA-Z0-9_-]{1,160}$/.test(value); }
4
+ function createReader(loadSdk, getQuery, dir) {
5
+ var sessionId = null, tasks = new Map(), closed = false;
6
+ function observe(event) {
7
+ if (!event) return;
8
+ if (event.session_id && !event.parent_tool_use_id) sessionId = event.session_id;
9
+ if (event.type !== 'system') return;
10
+ if (event.subtype === 'background_tasks_changed') {
11
+ tasks.forEach(function (task) { task.active = false; });
12
+ (event.tasks || []).slice(0, 128).forEach(function (task) {
13
+ if (!validId(task.task_id)) return;
14
+ var old = tasks.get(task.task_id);
15
+ if (old && String(old.run_id || '') === String(task.run_id || '') && old.task_type === task.task_type) Object.assign(old, task, { active: true });
16
+ else tasks.set(task.task_id, Object.assign({}, task, { active: true }));
17
+ });
18
+ }
19
+ if (event.subtype === 'task_notification') {
20
+ var old = tasks.get(event.task_id);
21
+ if (old && (!event.run_id || old.run_id === event.run_id)) old.active = false;
22
+ }
23
+ while (tasks.size > 128) tasks.delete(tasks.keys().next().value);
24
+ }
25
+ async function read(input) {
26
+ var task = tasks.get(input.taskId), query = getQuery(), parent = sessionId;
27
+ if (closed || !query || !task || !validId(input.taskId) || String(task.run_id || '') !== String(input.runId || '')) throw new Error('Task binding is unavailable.');
28
+ var offset = Number.isInteger(input.offset) && input.offset >= 0 && input.offset <= 10000 ? input.offset : 0;
29
+ function current() { if (closed || query !== getQuery() || parent !== sessionId || tasks.get(input.taskId) !== task) throw new Error('Task binding changed.'); }
30
+ var result;
31
+ if (task.task_type === 'local_agent') {
32
+ if (!parent || !dir) throw new Error('Task transcript is not ready.');
33
+ var sdk = await loadSdk(); current();
34
+ var agents = await sdk.listSubagents(parent, { dir: dir }); current();
35
+ // Never guess a sibling agent from its name, ordering or filesystem path.
36
+ if (agents.indexOf(task.task_id) < 0) throw new Error('Task transcript is not available yet.');
37
+ var messages = await sdk.getSubagentMessages(parent, task.task_id, { dir: dir, offset: offset, limit: 101 }); current();
38
+ result = { kind: 'agent', messages: projectMessages(messages.slice(0, 100), offset), offset: offset, hasMore: messages.length > 100, active: task.active };
39
+ } else {
40
+ if (typeof query.getTaskOutput !== 'function') throw new Error('This runtime does not expose task output.');
41
+ var output = await query.getTaskOutput(task.task_id); current();
42
+ result = { kind: 'shell', output: String(output.output || '').slice(-8192), truncated: !!output.truncated, active: task.active };
43
+ }
44
+ return result;
45
+ }
46
+ return { observe: observe, read: read, close: function () { closed = true; tasks.clear(); } };
47
+ }
48
+ function projectMessages(messages, offset) {
49
+ var rows = [], budget = 180000;
50
+ messages.forEach(function (message, index) {
51
+ var content = message.message && message.message.content;
52
+ if (typeof content === 'string') content = [{ type: 'text', text: content }];
53
+ if (!Array.isArray(content)) return;
54
+ content.slice(0, 100).forEach(function (block, blockIndex) {
55
+ if (budget <= 0) return;
56
+ var text, role = message.type === 'user' ? 'user' : 'assistant', type = block.type;
57
+ if (type === 'text') text = block.text;
58
+ else if (type === 'tool_use') { role = 'tool'; text = block.name + '\n' + JSON.stringify(block.input || {}); }
59
+ else if (type === 'tool_result') { role = 'tool'; text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || []); }
60
+ else return;
61
+ if (typeof text !== 'string') return;
62
+ text = text.slice(0, Math.min(16000, budget)); budget -= text.length;
63
+ rows.push({ role: role, text: text, recordIndex: ((offset || 0) + index) * 100 + blockIndex, recordType: type, toolName: block.name, toolId: block.id || block.tool_use_id, isError: !!block.is_error });
64
+ });
65
+ });
66
+ return rows;
67
+ }
68
+ function createRequests(worker) {
69
+ var pending = new Map();
70
+ function close() { pending.forEach(function (entry) { clearTimeout(entry.timer); entry.reject(new Error('Task connection closed.')); }); pending.clear(); }
71
+ return {
72
+ close: close,
73
+ request: function (input) {
74
+ if (pending.size >= 8) return Promise.reject(new Error('Task reader is busy.'));
75
+ return new Promise(function (resolve, reject) {
76
+ var id = crypto.randomUUID();
77
+ var timer = setTimeout(function () { pending.delete(id); reject(new Error('Task read timed out.')); }, 15000);
78
+ pending.set(id, { resolve: resolve, reject: reject, timer: timer });
79
+ try { if (worker.send({ type: 'read_background_task', requestId: id, input: input }) !== true) throw new Error('Task connection closed.'); }
80
+ catch (error) { pending.delete(id); clearTimeout(timer); reject(error); }
81
+ });
82
+ },
83
+ receive: function (message) {
84
+ var entry = pending.get(message.requestId); if (!entry) return;
85
+ pending.delete(message.requestId); clearTimeout(entry.timer);
86
+ if (message.error) entry.reject(new Error(message.error)); else entry.resolve(message.data);
87
+ }
88
+ };
89
+ }
90
+ async function respond(reader, message, send) {
91
+ try { if (!reader) throw new Error('Task query is unavailable.'); var data = await reader.read(message.input || {}); send({ type: 'background_task_response', requestId: message.requestId, data: data }); }
92
+ catch (error) { send({ type: 'background_task_response', requestId: message.requestId, error: error.message }); }
93
+ }
94
+ module.exports = { createReader: createReader, createRequests: createRequests, respond: respond, projectMessages: projectMessages };
@@ -198,6 +198,7 @@ var INITIALIZE_TIMEOUT_MS = 90000;
198
198
  * .setModel(model)
199
199
  * .setEffort(effort)
200
200
  * .setToolPolicy(policy) - "ask" | "allow-all"
201
+ * .readBackgroundTask({ taskId, runId, offset }) - optional bounded native transcript/output read
201
202
  * .stopTask(taskId)
202
203
  * .getContextUsage() - Promise<object|null>
203
204
  * .endInput() - stop accepting turns after queued input drains
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.3.0-beta.6",
3
+ "version": "5.3.0",
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"