clay-server 5.2.0-beta.6 → 5.2.0-beta.7

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 (108) hide show
  1. package/lib/clay-control-presentation-binding.js +96 -0
  2. package/lib/clay-control-presentation.js +128 -0
  3. package/lib/clay-control-target-projection.js +26 -0
  4. package/lib/clay-mate-chat.js +1 -0
  5. package/lib/clay-mate-knowledge.js +1 -1
  6. package/lib/clay-product-control-tools.js +1 -1
  7. package/lib/clay-product-control.js +22 -12
  8. package/lib/clay-project-records.js +2 -1
  9. package/lib/clay-scheduling.js +1 -1
  10. package/lib/clay-screen-controls.js +13 -5
  11. package/lib/clay-workspace-controls.js +2 -1
  12. package/lib/contextual-screen-snapshot.js +38 -0
  13. package/lib/mate-context-service.js +138 -0
  14. package/lib/mate-context-tools.js +21 -0
  15. package/lib/project-connection.js +8 -5
  16. package/lib/project-control-git-view.js +12 -0
  17. package/lib/project-filesystem.js +5 -1
  18. package/lib/project-http.js +3 -0
  19. package/lib/project-notifications.js +5 -4
  20. package/lib/project-product-control.js +3 -1
  21. package/lib/project-scheduled-messages.js +4 -4
  22. package/lib/project-scheduled-task-control.js +1 -1
  23. package/lib/project-session-document.js +20 -5
  24. package/lib/project-shared-browser.js +22 -5
  25. package/lib/project-studio-presentation.js +58 -0
  26. package/lib/project-user-message.js +36 -0
  27. package/lib/project-workspace-query.js +11 -1
  28. package/lib/project.js +24 -7
  29. package/lib/public/app.js +24 -0
  30. package/lib/public/appearance-bootstrap.js +2 -0
  31. package/lib/public/css/contextual-clay.css +240 -0
  32. package/lib/public/modules/app-favicon.js +7 -7
  33. package/lib/public/modules/app-message-router.js +5 -1
  34. package/lib/public/modules/app-messages.js +14 -1
  35. package/lib/public/modules/app-notifications.js +10 -3
  36. package/lib/public/modules/app-projects.js +3 -0
  37. package/lib/public/modules/clay-control-activity.js +214 -0
  38. package/lib/public/modules/clay-control-copy.js +36 -0
  39. package/lib/public/modules/clay-control-detail-targets.js +44 -0
  40. package/lib/public/modules/clay-control-object-targets.js +52 -0
  41. package/lib/public/modules/clay-control-target-visibility.js +12 -0
  42. package/lib/public/modules/clay-control-targets.js +44 -0
  43. package/lib/public/modules/clay-navigation-render.js +52 -0
  44. package/lib/public/modules/clay-owner-navigation.js +11 -2
  45. package/lib/public/modules/clay-session-links.js +31 -0
  46. package/lib/public/modules/clay-studio-bridge.js +20 -0
  47. package/lib/public/modules/clay-studio-document.js +67 -0
  48. package/lib/public/modules/clay-studio-render.js +144 -0
  49. package/lib/public/modules/clay-tool-presentation.js +4 -0
  50. package/lib/public/modules/command-palette.js +26 -31
  51. package/lib/public/modules/contextual-ask-session.js +26 -0
  52. package/lib/public/modules/contextual-clay-geometry.js +155 -0
  53. package/lib/public/modules/contextual-clay-session.js +130 -0
  54. package/lib/public/modules/contextual-clay-workspace.js +157 -0
  55. package/lib/public/modules/contextual-clay.js +237 -0
  56. package/lib/public/modules/contextual-composer.js +91 -0
  57. package/lib/public/modules/contextual-mate-context.js +115 -0
  58. package/lib/public/modules/contextual-mate-hint.js +40 -0
  59. package/lib/public/modules/contextual-mate-identity.js +23 -0
  60. package/lib/public/modules/contextual-mate-sessions.js +134 -0
  61. package/lib/public/modules/contextual-search.js +137 -0
  62. package/lib/public/modules/contextual-session-citations.js +58 -0
  63. package/lib/public/modules/filebrowser.js +20 -1
  64. package/lib/public/modules/generation-stop.js +8 -5
  65. package/lib/public/modules/git-panel.js +3 -1
  66. package/lib/public/modules/icon-strip-tooltip.js +2 -1
  67. package/lib/public/modules/input.js +35 -1
  68. package/lib/public/modules/issues-render.js +3 -2
  69. package/lib/public/modules/knowledge-db-workbench.js +4 -4
  70. package/lib/public/modules/markdown.js +2 -34
  71. package/lib/public/modules/mate-knowledge-workbench.js +1 -1
  72. package/lib/public/modules/message-delivery.js +10 -0
  73. package/lib/public/modules/pane-bridge.js +2 -2
  74. package/lib/public/modules/pending-message-queue.js +1 -0
  75. package/lib/public/modules/product-review.js +26 -0
  76. package/lib/public/modules/project-chat-identity.js +13 -4
  77. package/lib/public/modules/project-logs-render.js +3 -0
  78. package/lib/public/modules/project-mate-navigation.js +58 -21
  79. package/lib/public/modules/scheduled-tasks.js +2 -0
  80. package/lib/public/modules/search-clay-chat.js +5 -454
  81. package/lib/public/modules/shared-browser.js +2 -0
  82. package/lib/public/modules/sidebar-mobile.js +4 -3
  83. package/lib/public/modules/sidebar-projects.js +1 -0
  84. package/lib/public/modules/sidebar-sessions.js +1 -0
  85. package/lib/public/modules/split-view.js +7 -16
  86. package/lib/public/modules/terminal.js +9 -0
  87. package/lib/public/modules/tools.js +4 -1
  88. package/lib/public/modules/wireframe-workbench.js +5 -2
  89. package/lib/public/style.css +2 -0
  90. package/lib/sdk-bridge.js +4 -0
  91. package/lib/server-contextual-clay-settings.js +40 -0
  92. package/lib/server-contextual-mate-sessions.js +70 -0
  93. package/lib/server-home-chat.js +7 -1
  94. package/lib/server-home-clay-entry.js +23 -4
  95. package/lib/server-home-clay-session-links.js +12 -0
  96. package/lib/server-owner-ui.js +49 -8
  97. package/lib/server-settings.js +2 -0
  98. package/lib/server.js +17 -0
  99. package/lib/sessions.js +8 -1
  100. package/lib/terminal-manager.js +18 -11
  101. package/lib/users-contextual-clay-preferences.js +31 -0
  102. package/lib/users.js +3 -0
  103. package/lib/workspace-assignment-service.js +25 -0
  104. package/lib/workspace-query-mcp-server.js +1 -1
  105. package/lib/ws-request.js +1 -0
  106. package/lib/ws-schema.js +10 -0
  107. package/lib/yoke/claude-hook-installer.js +4 -0
  108. package/package.json +1 -1
@@ -125,7 +125,7 @@ export function handleKnowledgeDbClick(event, panel) {
125
125
  if (event.target.closest('[data-db-create-submit]')) { submitCreate(panel); return true; }
126
126
  if (event.target.closest('[data-db-create-cancel]')) { patchState(workspaceKey(), { createDraft: null }); patchMain(workspaceKey(), { fullscreen: false }); if (renderOwner) renderOwner({ forceDocument: true }); return true; }
127
127
  var reference = event.target.closest('[data-db-reference]'); if (reference) { var referenceKey = workspaceKey(); if (reference.dataset.dbReferenceType === 'text') { if (openOwnerDocument) openOwnerDocument('local:' + reference.dataset.dbReference); } else { var databaseIdentity = identityOwner ? identityOwner({ id: reference.dataset.dbReferenceDatabase, itemType: 'db', common: false }) : 'local:' + reference.dataset.dbReferenceDatabase; if (openOwnerDocument) openOwnerDocument(databaseIdentity); openRecord(reference.dataset.dbReferenceDatabase, reference.dataset.dbReference, referenceKey); } return true; }
128
- var row = event.target.closest('[data-db-record]'); if (row) { openRecord(row.dataset.dbDatabase, row.dataset.dbRecord); return true; }
128
+ var row = event.target.closest('tr[data-db-record]'); if (row) { openRecord(row.dataset.dbDatabase, row.dataset.dbRecord); return true; }
129
129
  var edit = event.target.closest('[data-db-edit-record]'); if (edit) { beginRecordEdit(edit.dataset.dbEditRecord, false); return true; }
130
130
  var add = event.target.closest('[data-db-add-record]'); if (add) { beginRecordEdit(add.dataset.dbAddRecord, true); return true; }
131
131
  var save = event.target.closest('[data-db-save-record]'); if (save) { submitRecord(panel, save.dataset.dbSaveRecord); return true; }
@@ -155,7 +155,7 @@ export function handleKnowledgeDbClick(event, panel) {
155
155
  }
156
156
 
157
157
  export function handleKnowledgeDbKeydown(event, panel) {
158
- var keyboardRow = event.target.closest && event.target.closest('[data-db-record]'); if (keyboardRow && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openRecord(keyboardRow.dataset.dbDatabase, keyboardRow.dataset.dbRecord); return true; }
158
+ var keyboardRow = event.target.closest && event.target.closest('tr[data-db-record]'); if (keyboardRow && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openRecord(keyboardRow.dataset.dbDatabase, keyboardRow.dataset.dbRecord); return true; }
159
159
  var dialog = panel && panel.querySelector('.knowledge-db-dialog'); if (!dialog) return false;
160
160
  if (event.key === 'Escape') { event.preventDefault(); closeArchiveDialog(panel); return true; }
161
161
  if (event.key !== 'Tab') return false; var controls = dialog.querySelectorAll('button:not(:disabled)'); if (!controls.length) return false; var first = controls[0]; var last = controls[controls.length - 1];
@@ -175,10 +175,10 @@ function renderDatabaseRecovery(body, toolbar, draft) { toolbar.innerHTML = '<sp
175
175
  function renderTable(body, dbState) {
176
176
  var database = dbState.database; var view = database.views.filter(function (item) { return item.id === dbState.selectedViewId; })[0] || database.views[0] || { columns: database.schema.filter(function (field) { return !field.archived; }).map(function (field) { return field.id; }) }; var fields = view.columns.map(function (id) { return fieldById(database, id); }).filter(Boolean);
177
177
  var html = '<div class="knowledge-db-table-wrap"><table class="knowledge-db-table"><thead><tr>' + fields.map(function (field) { return '<th scope="col">' + escapeText(field.name) + '</th>'; }).join('') + '</tr></thead><tbody>';
178
- html += dbState.records.map(function (record) { return '<tr tabindex="0" data-db-record="' + escapeText(record.id) + '" data-db-database="' + escapeText(database.id) + '">' + fields.map(function (field) { return '<td>' + displayValue(field, record.values[field.id], record.references) + '</td>'; }).join('') + '</tr>'; }).join('');
178
+ html += dbState.records.map(function (record) { return '<tr data-control-runtime="' + escapeText(store.get('controlRuntimeId')) + '" data-control-revision="' + escapeText(record.revision) + '" tabindex="0" data-db-record="' + escapeText(record.id) + '" data-db-database="' + escapeText(database.id) + '">' + fields.map(function (field) { return '<td>' + displayValue(field, record.values[field.id], record.references) + '</td>'; }).join('') + '</tr>'; }).join('');
179
179
  html += '</tbody></table>' + (!dbState.records.length && !dbState.loading ? '<p class="knowledge-empty">No records in this view.</p>' : '') + (dbState.nextCursor ? '<button type="button" class="knowledge-search-more" data-db-more="' + escapeText(database.id) + '">Load more</button>' : '') + '</div>'; body.innerHTML = html;
180
180
  }
181
- function renderRecord(body, dbState) { var detail = selectedRecordState(workspaceKey(), dbState.database.id); if (!detail) { body.innerHTML = '<p class="knowledge-empty">Loading record…</p>'; return; } var history = (detail.history || []).map(function (entry) { return '<li><b>' + escapeText(entry.action || 'update') + '</b><span>Revision ' + escapeText(entry.revision) + ' · ' + new Date(entry.at).toLocaleString() + '</span></li>'; }).join(''); body.innerHTML = '<article class="knowledge-db-record"><button type="button" data-db-record-back="' + escapeText(dbState.database.id) + '">' + iconHtml('arrow-left') + 'Table</button><h2>' + escapeText(detail.title) + '</h2><dl>' + dbState.database.schema.filter(function (field) { return !field.archived; }).map(function (field) { return '<dt>' + escapeText(field.name) + '</dt><dd>' + displayValue(field, detail.record.values[field.id], detail.record.references) + '</dd>'; }).join('') + '</dl><p class="knowledge-db-attribution">Revision ' + detail.record.revision + ' · updated ' + new Date(detail.record.updatedAt).toLocaleString() + '</p>' + (history ? '<h3>Recent history</h3><ol class="knowledge-db-history">' + history + '</ol>' : '') + '</article>'; }
181
+ function renderRecord(body, dbState) { var detail = selectedRecordState(workspaceKey(), dbState.database.id); if (!detail) { body.innerHTML = '<p class="knowledge-empty">Loading record…</p>'; return; } var history = (detail.history || []).map(function (entry) { return '<li><b>' + escapeText(entry.action || 'update') + '</b><span>Revision ' + escapeText(entry.revision) + ' · ' + new Date(entry.at).toLocaleString() + '</span></li>'; }).join(''); body.innerHTML = '<article class="knowledge-db-record" data-control-runtime="' + escapeText(store.get('controlRuntimeId')) + '" data-control-revision="' + escapeText(detail.record.revision) + '" data-db-record="' + escapeText(detail.record.id) + '" data-db-database="' + escapeText(dbState.database.id) + '"><button type="button" data-db-record-back="' + escapeText(dbState.database.id) + '">' + iconHtml('arrow-left') + 'Table</button><h2>' + escapeText(detail.title) + '</h2><dl>' + dbState.database.schema.filter(function (field) { return !field.archived; }).map(function (field) { return '<dt>' + escapeText(field.name) + '</dt><dd>' + displayValue(field, detail.record.values[field.id], detail.record.references) + '</dd>'; }).join('') + '</dl><p class="knowledge-db-attribution">Revision ' + detail.record.revision + ' · updated ' + new Date(detail.record.updatedAt).toLocaleString() + '</p>' + (history ? '<h3>Recent history</h3><ol class="knowledge-db-history">' + history + '</ol>' : '') + '</article>'; }
182
182
  function renderRecordForm(body, toolbar, dbState) { var edit = dbState.edit; var values = edit.values || {}; toolbar.innerHTML = '<span class="knowledge-preview-name">' + (edit.creating ? 'New record' : 'Edit record') + '</span>' + (!edit.creating ? '<button type="button" class="danger" data-db-archive-record="' + escapeText(dbState.database.id) + '">Archive</button>' : '') + '<button type="button" data-db-cancel-edit>Cancel</button><button type="button" class="primary" data-db-save-record="' + escapeText(dbState.database.id) + '">' + (edit.saving ? 'Retry save' : 'Save') + '</button>'; body.innerHTML = '<form class="knowledge-db-form" onsubmit="return false"><h2>' + (edit.creating ? 'New record' : 'Edit record') + '</h2>' + dbState.database.schema.filter(function (field) { return !field.archived; }).map(function (field) { return '<label>' + escapeText(field.name) + fieldInput(field, values[field.id]) + '</label>'; }).join('') + (edit.error ? '<p class="knowledge-inline-error">' + escapeText(edit.error) + '</p>' : '') + '</form>'; }
183
183
  function renderSettings(body, toolbar, dbState) { var edit = dbState.edit; toolbar.innerHTML = '<span class="knowledge-preview-name">Database settings</span><button type="button" data-db-cancel-edit>Cancel</button><button type="button" class="primary" data-db-save-settings="' + escapeText(dbState.database.id) + '">' + (edit.saving ? 'Retry rename' : 'Save name') + '</button>'; body.innerHTML = '<form class="knowledge-db-form" onsubmit="return false"><h2>Database settings</h2><label>Name<input data-db-settings-name value="' + escapeText(edit.name) + '" maxlength="120"></label>' + (edit.error ? '<p class="knowledge-inline-error">' + escapeText(edit.error) + '</p>' : '') + '<section class="knowledge-danger-zone"><h3>Archive database</h3><p>Records, schema, views, and history remain recoverable.</p><button type="button" class="danger" data-db-archive-database="' + escapeText(dbState.database.id) + '">Archive database</button></section></form>'; }
184
184
  function renderRecover(body, toolbar, dbState) { var edit = dbState.edit; toolbar.innerHTML = '<span class="knowledge-preview-name">Recover record</span><button type="button" data-db-cancel-edit>Cancel</button><button type="button" class="primary" data-db-confirm-recover-record="' + escapeText(dbState.database.id) + '">' + (edit.saving ? 'Retry recovery' : 'Recover') + '</button>'; body.innerHTML = '<div class="knowledge-db-form"><h2>Recover archived record?</h2><p>This restores the exact record at revision ' + escapeText(edit.baseVersion) + '.</p>' + (edit.error ? '<p class="knowledge-inline-error">' + escapeText(edit.error) + '</p>' : '') + '</div>'; }
@@ -1,3 +1,4 @@
1
+ import { enhanceSessionCitations } from './clay-session-links.js';
1
2
  import { renderBrowserLink, enhanceBrowserLinks } from './clay-browser-links.js';
2
3
  import { renderWireframeBlocks } from './wireframe-chat.js';
3
4
  import { copyToClipboard, escapeHtml } from './utils.js';
@@ -70,40 +71,7 @@ export function renderMarkdown(text) {
70
71
 
71
72
  export function enhanceClaySessionLinks(root) {
72
73
  if (!root || typeof document === "undefined") return;
73
- var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
74
- var nodes = [];
75
- var node;
76
- while ((node = walker.nextNode())) {
77
- if (!node.parentElement || node.parentElement.closest("code, pre, a, button")) continue;
78
- if (/\[clayos\/session:[A-Za-z0-9_-]{24}(?:\s+[\u2014-]\s+[^\]\n]{1,80})?\]/.test(node.nodeValue || "")) nodes.push(node);
79
- }
80
- for (var i = 0; i < nodes.length; i++) {
81
- var text = nodes[i].nodeValue || "";
82
- var pattern = /\[clayos\/(session:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]/g;
83
- var fragment = document.createDocumentFragment();
84
- var offset = 0;
85
- var match;
86
- while ((match = pattern.exec(text))) {
87
- fragment.appendChild(document.createTextNode(text.slice(offset, match.index)));
88
- var button = document.createElement("button");
89
- button.type = "button";
90
- button.className = "clayos-session-link clayos-record-link";
91
- button.dataset.sessionRef = match[1];
92
- button.setAttribute("aria-label", "Open session" + (match[2] ? " from " + match[2].trim() : ""));
93
- var label = document.createElement("span");
94
- label.textContent = "Session";
95
- button.appendChild(label);
96
- if (match[2]) {
97
- var meta = document.createElement("small");
98
- meta.textContent = match[2].trim();
99
- button.appendChild(meta);
100
- }
101
- fragment.appendChild(button);
102
- offset = pattern.lastIndex;
103
- }
104
- fragment.appendChild(document.createTextNode(text.slice(offset)));
105
- nodes[i].parentNode.replaceChild(fragment, nodes[i]);
106
- }
74
+ enhanceSessionCitations(root);
107
75
  enhanceClayLogLinks(root);
108
76
  enhanceClayFileLinks(root);
109
77
  enhanceBrowserLinks(root);
@@ -350,7 +350,7 @@ function renderExplorer() {
350
350
  files.forEach(function (file) {
351
351
  var parts = file.name.split('/'); var folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
352
352
  if (!state.query.trim() && folder !== lastFolder) { var folderRow = document.createElement('div'); folderRow.className = 'knowledge-folder-row'; folderRow.innerHTML = iconHtml('folder') + '<span>' + escapeText(folder || 'Top level') + '</span>'; list.appendChild(folderRow); lastFolder = folder; }
353
- var id = documentIdentity(file); var button = document.createElement('button'); button.type = 'button'; button.className = 'knowledge-file-row'; button.dataset.openDocument = id; button.style.setProperty('--knowledge-depth', String(Math.min(parts.length - 1, 6))); if (id === state.selected) button.setAttribute('aria-current', 'true');
353
+ var id = documentIdentity(file); var button = document.createElement('button'); button.type = 'button'; button.className = 'knowledge-file-row'; button.dataset.openDocument = id; button.dataset.controlRevision = String(file.revision || ''); button.dataset.controlRuntime = store.get('controlRuntimeId') || ''; button.style.setProperty('--knowledge-depth', String(Math.min(parts.length - 1, 6))); if (id === state.selected) button.setAttribute('aria-current', 'true');
354
354
  button.innerHTML = iconHtml(file.itemType === 'db' || file.name.endsWith('.jsonl') ? 'database' : 'file-text') + '<span>' + escapeText(parts[parts.length - 1].replace(/\.(md|jsonl)$/i, '')) + (file.snippet ? '<em>' + escapeText(file.snippet) + '</em>' : '') + '</span>' + (file.common ? '<small>shared</small>' : ''); list.appendChild(button);
355
355
  });
356
356
  if (state.query.trim() && state.searchFiles.length < state.searchTotal) { var more = document.createElement('button'); more.type = 'button'; more.className = 'knowledge-search-more'; more.dataset.searchMore = ''; more.disabled = state.searching; more.textContent = state.searching ? 'Loading more…' : 'Load more results'; list.appendChild(more); }
@@ -179,6 +179,7 @@ export function sendAcknowledgedMessage(payload) {
179
179
  nextPayload.cliSessionId = entry.cliSessionId;
180
180
  nextPayload.sessionOriginId = entry.sessionOriginId;
181
181
  entry.payload = nextPayload;
182
+ if (nextPayload.contextualSnapshot) store.set({ contextualSnapshotSubmissions: (store.get('contextualSnapshotSubmissions') || []).concat([{ clientMessageId: nextPayload.clientMessageId, accountId: entry.accountId, projectSlug: entry.projectSlug, sessionId: entry.sessionId, sessionOriginId: entry.sessionOriginId, snapshot: nextPayload.contextualSnapshot }]).slice(-64) });
182
183
  replacePending(pendingMessages().concat([entry]));
183
184
  if (!transmit(entry, false)) {
184
185
  if (!getWs() || getWs().readyState !== 1) markUnconfirmed(entry);
@@ -204,6 +205,15 @@ export function retryPendingMessage(clientMessageId) {
204
205
  return false;
205
206
  }
206
207
 
208
+ // Stop retains the uncertain receipt but prevents automatic retransmission.
209
+ export function pausePendingMessage(clientMessageId) {
210
+ var entry = pendingEntry(clientMessageId);
211
+ if (!entry || !matchesCurrentContext(entry)) return false;
212
+ clearAckTimer(clientMessageId);
213
+ markUnconfirmed(entry);
214
+ return true;
215
+ }
216
+
207
217
  export function acknowledgeMessage(clientMessageId) {
208
218
  if (typeof clientMessageId !== "string") return null;
209
219
  var pending = pendingMessages();
@@ -1,3 +1,4 @@
1
+ import { forwardStudioPresentation } from './clay-studio-render.js';
1
2
  // Pane-side postMessage bridge for the split-view shell. A chrome-less pane
2
3
  // iframe reports its context usage up to the parent (which renders a chip in
3
4
  // the pane header) and honors panel-toggle requests coming back down.
@@ -32,8 +33,7 @@ export function forwardPaneAuthRequired(message) {
32
33
 
33
34
  export function forwardPaneMarkdownPresentation(message) {
34
35
  if (!store.get('paneMode') || window.parent === window) return false;
35
- window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
36
- return true;
36
+ return forwardStudioPresentation(message, "clay-pane-present-markdown");
37
37
  }
38
38
 
39
39
  export function forwardPaneIssueReference(ref, projectSlug) {
@@ -330,6 +330,7 @@ function renderItem(item, index) {
330
330
  var disabled = controlsDisabled ? ' disabled aria-disabled="true"' : '';
331
331
  var text = (item.message && item.message.text || "").trim() || "Attachment-only message";
332
332
  var html = '<div class="pending-message-chip' + (item.state === "claimed" ? ' is-claimed' : '') + (scheduled ? ' is-scheduled' : '') + (futureScheduled ? ' is-future' : '') + '" data-pending-id="' + escapeHtml(item.id) + '">';
333
+ if (item.schedule && item.schedule.logicalScheduleId) html = html.replace('data-pending-id=', 'data-control-schedule-id="' + escapeHtml(item.schedule.logicalScheduleId) + '" data-control-revision="' + escapeHtml(item.schedule.logicalRevision || '') + '" data-control-runtime="' + escapeHtml(store.get('controlRuntimeId') || '') + '" data-pending-id=');
333
334
  html += '<span class="pending-message-number" aria-hidden="true">' + (index + 1) + '</span>';
334
335
  if (own && item.state === "pending") html += '<button class="pending-message-drag" type="button" data-action="drag" aria-label="Reorder queued message ' + (index + 1) + '" title="Drag, or use arrow keys to reorder"' + disabled + '>' + '<i data-lucide="grip-vertical"></i></button>';
335
336
  else html += '<span class="pending-message-drag is-static"><i data-lucide="' + (item.state === "claimed" ? "loader-circle" : "lock-keyhole") + '"></i></span>';
@@ -0,0 +1,26 @@
1
+ // Display-only metadata from a bound product service. The exact question and
2
+ // authenticated response remain unchanged; this renderer grants no authority.
3
+ export function renderProductReview(element, question) {
4
+ var review = question.review;
5
+ if (!review || review.kind !== 'product-control' || typeof review.action !== 'string' || typeof review.target !== 'string' || typeof review.context !== 'string' || typeof review.reason !== 'string') return false;
6
+ element.classList.add('product-review');
7
+ element.textContent = '';
8
+ function text(tag, className, value) {
9
+ var node = document.createElement(tag);
10
+ node.className = className;
11
+ node.textContent = value;
12
+ element.appendChild(node);
13
+ }
14
+ text('strong', 'product-review-action', review.action + ': ' + review.target);
15
+ text('p', 'product-review-context', review.context);
16
+ text('p', 'product-review-reason', review.reason);
17
+ var details = document.createElement('details');
18
+ var summary = document.createElement('summary');
19
+ summary.textContent = 'Exact request details';
20
+ var payload = document.createElement('pre');
21
+ payload.textContent = question.question;
22
+ details.appendChild(summary);
23
+ details.appendChild(payload);
24
+ element.appendChild(details);
25
+ return true;
26
+ }
@@ -4,6 +4,9 @@ import { store } from './store.js';
4
4
  import { mateAvatarUrl } from './avatar.js';
5
5
  import { VENDOR_AVATARS, VENDOR_NAMES } from './chat-render-runtime.js';
6
6
 
7
+ // A missing or failed profile image must never borrow another speaker's identity.
8
+ var NEUTRAL_AVATAR = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#888" fill-opacity=".14"/><circle cx="16" cy="12" r="5" fill="#888"/><path d="M6 29a10 10 0 0 1 20 0" fill="#888"/></svg>');
9
+
7
10
  function mateById(state, mateId) {
8
11
  var mates = state.cachedMatesList || [];
9
12
  for (var i = 0; i < mates.length; i++) {
@@ -24,12 +27,11 @@ export function resolveProjectAssistantIdentity(state, vendor) {
24
27
  if (mateId) {
25
28
  var mate = mateById(state, mateId) || dmTarget;
26
29
  var fallbackName = dmTarget && dmTarget.displayName || state.activeSessionMateName || 'Mate';
27
- var avatarMate = mate || { id: mateId, name: fallbackName, profile: { displayName: fallbackName } };
28
30
  return {
29
31
  kind: 'mate',
30
32
  mateId: mateId,
31
33
  name: mateName(mate, fallbackName),
32
- avatarUrl: mateAvatarUrl(avatarMate, 34),
34
+ avatarUrl: mate ? mateAvatarUrl(mate, 34) : NEUTRAL_AVATAR,
33
35
  };
34
36
  }
35
37
  var vendorId = vendor || state.currentVendor || 'claude';
@@ -47,9 +49,16 @@ export function getProjectAssistantPlaceholder(state, vendor) {
47
49
 
48
50
  export function applyProjectAssistantIdentity(element, identity) {
49
51
  if (!element || !identity) return element;
50
- var avatar = element.querySelector('.dm-bubble-avatar');
52
+ var avatar = element.querySelector('.dm-bubble-avatar') || element.querySelector('.activity-inline-avatar');
51
53
  var name = element.querySelector('.dm-bubble-name');
52
- if (avatar) avatar.src = identity.avatarUrl;
54
+ if (avatar) {
55
+ avatar.onerror = identity.kind === 'mate' ? function () { avatar.onerror = null; avatar.src = NEUTRAL_AVATAR; } : null;
56
+ avatar.src = identity.avatarUrl;
57
+ }
58
+ if (element.className.indexOf('activity-inline') >= 0) {
59
+ var activityLabel = element.querySelector('.activity-state-label');
60
+ element.setAttribute('aria-label', identity.name + ' · ' + (activityLabel ? activityLabel.textContent : 'Responding'));
61
+ }
53
62
  if (name) name.textContent = identity.name;
54
63
  if (identity.kind === 'mate') {
55
64
  element.dataset.sessionMateId = identity.mateId;
@@ -1,3 +1,4 @@
1
+ import { store } from './store.js';
1
2
  // Project Logs rendering — ledger rows, entry detail, and discussion.
2
3
  //
3
4
  // Split from project-logs.js so both stay well inside the module size limit.
@@ -142,6 +143,8 @@ function renderRow(entry, onOpen) {
142
143
  row.type = "button";
143
144
  row.className = "project-log-row";
144
145
  row.dataset.ref = entry.ref;
146
+ row.dataset.controlRuntime = store.get("controlRuntimeId") || "";
147
+ row.dataset.controlRevision = String(entry.revisions);
145
148
  row.setAttribute("role", "listitem");
146
149
 
147
150
  var head = document.createElement("div");
@@ -1,3 +1,6 @@
1
+ import { contextualMateStamp, contextualMate } from './contextual-mate-identity.js';
2
+ import { finishContextualWorkspace } from './contextual-clay-workspace.js';
3
+ import { initContextualClay, cycleContextualClay, contextualClayMode } from './contextual-clay.js';
1
4
  // Existing Mates share the project rail while retaining their conversations.
2
5
  import { store } from './store.js';
3
6
  import { applyWorkspaceColor } from './mate-workspace-color.js';
@@ -103,7 +106,7 @@ function renderMateHeaderDefault(state) {
103
106
 
104
107
  function ordinaryProjects(state) {
105
108
  return (state.projectsHubList || []).filter(function (project) {
106
- return project && project.slug && project.isMate !== true;
109
+ return project && project.slug && project.isMate !== true && project.contentAccess !== false;
107
110
  });
108
111
  }
109
112
 
@@ -124,7 +127,7 @@ export function clayReturnTargetForState(state) {
124
127
  };
125
128
  }
126
129
 
127
- function rememberClayReturnTarget(state) {
130
+ export function rememberClayReturnTarget(state) {
128
131
  if (!state || state.homeShellVisible || state.dmMode || isMateWorkspace(state) || !state.currentSlug ||
129
132
  state.activeProjectSlug !== state.currentSlug || state.sessionActivatedProjectSlug !== state.currentSlug) return;
130
133
  var projects = ordinaryProjects(state);
@@ -135,15 +138,18 @@ function rememberClayReturnTarget(state) {
135
138
  return target;
136
139
  }
137
140
 
138
- function returnFromClay() {
141
+ export function returnFromClay() {
142
+ var handoff = store.get('contextualWorkspace');
143
+ var originalMate = handoff && handoff.returnSlug && handoff.returnSlug.indexOf('mate-') === 0 && contextualMate(handoff.returnSlug.slice(5));
144
+ if (finishContextualWorkspace() === false) return false;
139
145
  var state = store.snap();
140
- var target = clayReturnTargetForState(state);
146
+ var target = originalMate ? { projectSlug: handoff.returnSlug, sessionId: handoff.returnSessionId } : clayReturnTargetForState(state);
141
147
  if (!target) {
142
148
  store.set({ pendingProjectSessionNavigation: null, clayReturnPendingProjectSlug: null, clayReturnTarget: null });
143
149
  showProjectsHub();
144
150
  return false;
145
151
  }
146
- store.set({ clayReturnTarget: null, clayReturnPendingProjectSlug: target.sessionId ? target.projectSlug : null });
152
+ store.set({ clayReturnTarget: originalMate ? state.clayReturnTarget : null, clayReturnPendingProjectSlug: target.sessionId ? target.projectSlug : null });
147
153
  if (target.sessionId) openProjectSession(target.projectSlug, target.sessionId);
148
154
  else {
149
155
  store.set({ pendingProjectSessionNavigation: null });
@@ -156,7 +162,8 @@ function updateClayBrand(state) {
156
162
  var entry = document.querySelector('.icon-strip-brand');
157
163
  if (!entry) return;
158
164
  var clay = navigationMates(state.cachedMatesList).find(function (mate) { return mate.builtinKey === 'clay'; });
159
- var active = clayIsActive(state, clay);
165
+ var ownPopover = (state.contextualClay || {}).mateId === (clay && clay.id) && contextualClayMode() !== 'closed';
166
+ var active = clayIsActive(state, clay) || ownPopover;
160
167
  var speaking = !!(state.youActivity && state.youActivity.active);
161
168
  entry.dataset.clayMode = active ? 'mate' : 'project';
162
169
  entry.classList.toggle('clay-speaking', speaking);
@@ -166,20 +173,40 @@ function updateClayBrand(state) {
166
173
  if (logo) logo.src = 'clay-studio-symbol.png';
167
174
  entry.disabled = !clay;
168
175
  entry.classList.toggle('active', active);
169
- entry.setAttribute('aria-label', clay ? (active ? 'Return to project' : 'Open Clay') : 'Clay is unavailable');
170
- if (active) entry.setAttribute('aria-current', 'page');
176
+ entry.setAttribute('aria-label', clay ? (ownPopover && contextualClayMode() === 'popover' ? 'Open Clay workspace' : active ? 'Return to project' : 'Open Clay') : 'Clay is unavailable');
177
+ entry.setAttribute('aria-expanded', String(ownPopover));
178
+ entry.setAttribute('aria-controls', 'contextual-clay');
179
+ if (clayIsActive(state, clay)) entry.setAttribute('aria-current', 'page');
171
180
  else entry.removeAttribute('aria-current');
172
181
  }
173
182
 
174
- export function openMateWorkspace(mateId) {
183
+ export function openMateWorkspace(mateId, sessionId) {
175
184
  var mate = navigationMates(store.get('cachedMatesList')).find(function (item) { return item.id === mateId; });
176
- if (!mate) return;
185
+ if (!mate || !contextualMate(mateId)) return;
177
186
  if (mate.builtinKey === 'clay' && store.snap) rememberClayReturnTarget(store.snap());
178
187
  if (store.get('currentSlug') !== 'mate-' + mateId) {
179
188
  closeTerminal();
180
189
  closeFileViewer();
181
190
  }
182
- switchProject('mate-' + mateId);
191
+ if (Number.isSafeInteger(sessionId) && sessionId > 0) openProjectSession('mate-' + mateId, sessionId);
192
+ else switchProject('mate-' + mateId);
193
+ }
194
+
195
+ export function activateContextualMate(mateId) {
196
+ var mate = contextualMate(mateId);
197
+ if (!mate) return;
198
+ var handoff = store.get('contextualWorkspace');
199
+ if (handoff && handoff.stage !== 'returning') {
200
+ if (handoff.stage !== 'workspace') return;
201
+ var popup = store.get('contextualClay') || {};
202
+ var otherSession = popup.mode === 'popover' && popup.mateId === mateId && popup.sessionId && popup.sessionId !== handoff.sessionId;
203
+ if (handoff.slug === 'mate-' + mateId && !otherSession && !(popup.mode === 'popover' && popup.mateId === mateId)) return returnFromClay();
204
+ cycleContextualClay(mate);
205
+ return;
206
+ }
207
+ if (handoff) { store.set({ contextualMatePending: mateId }); return; }
208
+ rememberClayReturnTarget(store.snap());
209
+ cycleContextualClay(mate);
183
210
  }
184
211
 
185
212
  function createMateIcon(mateId) {
@@ -190,9 +217,10 @@ function createMateIcon(mateId) {
190
217
  button.innerHTML = '<img class="icon-strip-mate-avatar" alt="" draggable="false"><span class="icon-strip-pill"></span><span class="icon-strip-status"></span><span class="icon-strip-project-badge"></span>';
191
218
  button.addEventListener('click', function () {
192
219
  hideIconTooltip();
193
- openMateWorkspace(mateId);
220
+ activateContextualMate(mateId);
194
221
  });
195
222
  bindIconTooltip(button, function () {
223
+ if ((store.get('contextualClay') || {}).mode === 'popover' && (store.get('contextualClay') || {}).mateId === mateId) return { text: '' };
196
224
  var mate = navigationMates(store.get('cachedMatesList')).find(function (item) { return item.id === mateId; });
197
225
  return {
198
226
  text: mate ? getHomeMateName(mate) : 'Mate',
@@ -225,21 +253,25 @@ export function renderProjectMates() {
225
253
  applyWorkspaceColor(mateForWorkspace(state));
226
254
  renderMateHeaderDefault(state);
227
255
  var mates = navigationMates(state.cachedMatesList).filter(function (mate) {
228
- return mate.builtinKey !== 'clay';
256
+ return mate.builtinKey !== 'clay' && contextualMate(mate.id);
229
257
  });
230
258
  var mentionActive = getActiveMentionMateIds();
231
259
  var projects = state.projectsHubList || [];
232
260
  var unread = state.dmUnread || {};
233
261
  var icons = {};
234
- rail.querySelectorAll('[data-mate-id]').forEach(function (button) { icons[button.dataset.mateId] = button; });
262
+ document.querySelectorAll('.icon-strip-project-mate[data-mate-id]').forEach(function (button) { icons[button.dataset.mateId] = button; });
235
263
  mates.forEach(function (mate) {
236
264
  var button = icons[mate.id] || createMateIcon(mate.id);
237
265
  delete icons[mate.id];
266
+ button.dataset.controlMateStamp = contextualMateStamp(mate);
238
267
  var active = !state.homeShellVisible && !state.dmMode && state.currentSlug === 'mate-' + mate.id;
239
268
  var project = projects.find(function (item) { return item.slug === 'mate-' + mate.id; });
240
269
  var busy = !!(project && project.isProcessing) || !!mentionActive[mate.id];
241
270
  var count = active ? 0 : (unread[mate.id] || 0);
242
- button.classList.toggle('active', !!active);
271
+ var popover = (state.contextualClay || {}).mateId === mate.id && contextualClayMode() === 'popover';
272
+ button.classList.toggle('active', !!active || popover);
273
+ button.setAttribute('aria-expanded', String(popover));
274
+ button.setAttribute('aria-controls', 'contextual-clay');
243
275
  button.setAttribute('aria-label', getHomeMateName(mate) + ', Mate' + (busy ? ', working' : '') + (count ? ', ' + count + ' unread' : ''));
244
276
  if (active) button.setAttribute('aria-current', 'page');
245
277
  else button.removeAttribute('aria-current');
@@ -257,6 +289,7 @@ export function renderProjectMates() {
257
289
  Object.keys(icons).forEach(function (id) { icons[id].remove(); });
258
290
  var ordered = Array.from(rail.querySelectorAll('[data-mate-id]'));
259
291
  mates.forEach(function (mate, index) {
292
+ if (document.querySelector('.contextual-mate-mobile-trigger[data-mate-id="' + CSS.escape(mate.id) + '"]')) return;
260
293
  var button = ordered.find(function (item) { return item.dataset.mateId === mate.id; });
261
294
  if (ordered[index] === button) return;
262
295
  rail.insertBefore(button, ordered[index] || null);
@@ -284,21 +317,25 @@ export function renderProjectMates() {
284
317
  }
285
318
 
286
319
  export function initProjectMateNavigation() {
320
+ initContextualClay();
287
321
  var clayEntry = document.querySelector('.icon-strip-brand');
288
322
  if (clayEntry) {
289
- clayEntry.addEventListener('click', function () {
323
+ clayEntry.addEventListener('click', function (event) {
290
324
  var state = store.snap();
291
325
  var clay = navigationMates(state.cachedMatesList).find(function (mate) { return mate.builtinKey === 'clay'; });
292
326
  hideIconTooltip();
293
327
  if (!clay) return;
294
- if (!clayIsActive(state, clay)) {
295
- openMateWorkspace(clay.id);
296
- } else returnFromClay();
328
+ var popup = state.contextualClay || {};
329
+ if (clayIsActive(state, clay) && !(popup.mode === 'popover' && popup.mateId === clay.id)) returnFromClay();
330
+ else {
331
+ activateContextualMate(clay.id);
332
+ }
297
333
  });
298
334
  bindIconTooltip(clayEntry, function () {
299
335
  var state = store.snap();
300
336
  var clay = navigationMates(state.cachedMatesList).find(function (mate) { return mate.builtinKey === 'clay'; });
301
- return { text: clayIsActive(state, clay) ? 'Return to project' : 'Open Clay', kind: 'mate', avatarUrl: 'clay-studio-symbol.png' };
337
+ if ((state.contextualClay || {}).mateId === (clay && clay.id) && contextualClayMode() === 'popover') return { text: '' };
338
+ return { text: (state.contextualClay || {}).mateId === (clay && clay.id) && contextualClayMode() === 'popover' ? 'Open Clay workspace' : clayIsActive(state, clay) ? 'Return to project' : 'Open Clay', kind: 'mate', avatarUrl: 'clay-studio-symbol.png' };
302
339
  });
303
340
  }
304
341
  var knob = document.getElementById('icon-strip-filter-knob');
@@ -313,7 +350,7 @@ export function initProjectMateNavigation() {
313
350
  if (state.cachedMatesList !== previous.cachedMatesList || state.homeChatMateId !== previous.homeChatMateId ||
314
351
  state.homeShellVisible !== previous.homeShellVisible || state.projectsHubList !== previous.projectsHubList ||
315
352
  state.dmUnread !== previous.dmUnread || state.currentSlug !== previous.currentSlug ||
316
- state.dmMode !== previous.dmMode || state.connected !== previous.connected || state.activeSessionId !== previous.activeSessionId || state.youActivity !== previous.youActivity) renderProjectMates();
353
+ state.contextualClay !== previous.contextualClay || state.dmMode !== previous.dmMode || state.connected !== previous.connected || state.activeSessionId !== previous.activeSessionId || state.youActivity !== previous.youActivity) renderProjectMates();
317
354
  if (state.projectMateFilterMode !== previous.projectMateFilterMode ||
318
355
  state.projectMateFilterTurns !== previous.projectMateFilterTurns) applyProjectMateFilter();
319
356
  });
@@ -170,6 +170,8 @@ function buildRow(record) {
170
170
  var row = document.createElement("article");
171
171
  row.className = "scheduled-task-row" + (record.enabled === false ? " paused" : "");
172
172
  row.dataset.taskId = record.id;
173
+ row.dataset.controlRuntime = store.get("controlRuntimeId") || "";
174
+ row.dataset.controlRevision = String(record.updatedAt);
173
175
  row.setAttribute("role", "listitem");
174
176
  var stateLabel = scheduledTaskExecutionLabel(record);
175
177
  row.innerHTML =