clay-server 4.1.0-beta.9 → 4.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/daemon-project-access.js +14 -0
  3. package/lib/daemon.js +7 -0
  4. package/lib/issues-mcp-server.js +1 -1
  5. package/lib/issues-service.js +3 -1
  6. package/lib/loop-registry-occurrence.js +1 -0
  7. package/lib/notes.js +20 -3
  8. package/lib/os-users.js +6 -0
  9. package/lib/project-connection.js +14 -3
  10. package/lib/project-file-path.js +13 -0
  11. package/lib/project-file-watch.js +19 -8
  12. package/lib/project-filesystem.js +10 -16
  13. package/lib/project-http.js +53 -48
  14. package/lib/project-issue-launch.js +3 -1
  15. package/lib/project-logs-mcp-server.js +8 -8
  16. package/lib/project-logs-schema.js +1 -1
  17. package/lib/project-logs-service.js +46 -13
  18. package/lib/project-logs-store.js +17 -15
  19. package/lib/project-logs.js +70 -0
  20. package/lib/project-loop.js +1 -0
  21. package/lib/project-notifications.js +1 -1
  22. package/lib/project-scheduled-result-store.js +131 -0
  23. package/lib/project-scheduled-task-execution.js +35 -2
  24. package/lib/project-session-notes.js +11 -4
  25. package/lib/project-sessions.js +39 -6
  26. package/lib/project-user-message.js +47 -0
  27. package/lib/project.js +57 -3
  28. package/lib/public/app.js +27 -9
  29. package/lib/public/css/avatar-imprints.css +1 -1
  30. package/lib/public/css/command-palette.css +57 -19
  31. package/lib/public/css/git-panel.css +13 -12
  32. package/lib/public/css/git-placard.css +4 -4
  33. package/lib/public/css/issues.css +79 -32
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/project-logs.css +15 -0
  37. package/lib/public/css/sidebar.css +7 -0
  38. package/lib/public/css/sticky-notes.css +29 -0
  39. package/lib/public/index.html +10 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +50 -12
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +2 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -61
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -0
  50. package/lib/public/modules/clay-issue-links.js +24 -2
  51. package/lib/public/modules/clay-log-links.js +12 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/issues-render.js +36 -10
  57. package/lib/public/modules/issues.js +95 -13
  58. package/lib/public/modules/markdown.js +3 -1
  59. package/lib/public/modules/pane-bridge.js +14 -8
  60. package/lib/public/modules/pane-issue-bridge.js +27 -0
  61. package/lib/public/modules/permission-hints.js +13 -0
  62. package/lib/public/modules/profile.js +9 -9
  63. package/lib/public/modules/project-activation.js +49 -0
  64. package/lib/public/modules/project-logs-render.js +13 -3
  65. package/lib/public/modules/project-logs.js +45 -15
  66. package/lib/public/modules/project-session-visibility-settings.js +138 -0
  67. package/lib/public/modules/project-settings.js +5 -0
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-result-client.js +33 -0
  70. package/lib/public/modules/scheduled-result-display.js +50 -0
  71. package/lib/public/modules/scheduled-result-notification.js +31 -0
  72. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  73. package/lib/public/modules/scheduled-tasks.js +9 -21
  74. package/lib/public/modules/sidebar-mobile.js +51 -3
  75. package/lib/public/modules/sidebar-projects.js +14 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +31 -7
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-title-generator.js +89 -0
  96. package/lib/session-title-policy.js +8 -2
  97. package/lib/session-visibility.js +62 -1
  98. package/lib/sessions.js +28 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +13 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. package/package.json +5 -7
@@ -1,22 +1,76 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
3
  import { showToast } from './utils.js';
4
- import { closeProjectLogs } from './project-logs.js';
5
- import { closeScheduledTasks } from './scheduled-tasks.js';
6
- import { closeNotesBrowser } from './sticky-notes-browser.js';
7
- import { closeFileViewer } from './filebrowser.js';
8
- import { closeTerminal } from './terminal.js';
4
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
9
5
  import { refreshIcons } from './icons.js';
10
- import { renderIssueList, renderIssueDetail, renderIssueForm, renderIssueHistory, statusLabel } from './issues-render.js';
6
+ import { enhanceClayLogLinks } from './clay-log-links.js';
7
+ import { updateIssueLink } from './clay-issue-links.js';
8
+ import { renderIssueList, renderIssueDetail, renderIssueForm, renderIssueHistory } from './issues-render.js';
11
9
  function panel() { return document.getElementById('issues-panel'); }
12
10
  function content() { return panel().querySelector('.issues-content'); }
11
+ function clearUpdateNotice() {
12
+ store.set({ issuesUpdateAvailable: null });
13
+ var action = content() && content().querySelector('[data-issue-update-notice]');
14
+ if (action) action.remove();
15
+ }
16
+ function renderContextualUpdate() {
17
+ var update = store.get('issuesUpdateAvailable');
18
+ var issueContent = content();
19
+ var actions = issueContent && (issueContent.querySelector('.issue-detail .issue-actions') || issueContent.querySelector('#issue-form .issue-actions'));
20
+ if (!actions || !update) return;
21
+ var existing = actions.querySelector('[data-issue-update-notice]');
22
+ if (existing) existing.remove();
23
+ var notice = document.createElement('span');
24
+ notice.className = 'issues-inline-update';
25
+ notice.dataset.issueUpdateNotice = 'true';
26
+ notice.appendChild(document.createTextNode(store.get('issuesView') === 'edit' ? 'Updated remotely · Finish editing to refresh' : 'Updated remotely'));
27
+ var refresh = document.createElement('button');
28
+ refresh.type = 'button';
29
+ refresh.dataset.issueRefresh = update.ref;
30
+ refresh.textContent = 'Refresh';
31
+ refresh.disabled = store.get('issuesView') === 'edit';
32
+ if (refresh.disabled) refresh.title = 'Finish editing to refresh';
33
+ notice.appendChild(refresh);
34
+ actions.insertBefore(notice, actions.firstChild);
35
+ }
36
+ function captureDraft() {
37
+ var state = store.snap();
38
+ if (state.issuesView !== 'detail' && state.issuesView !== 'edit') return null;
39
+ var draft = { ref: entryRef(), view: state.issuesView, comment: null };
40
+ var comment = panel() && panel().querySelector('#issue-comment-input');
41
+ if (comment) draft.comment = comment.value;
42
+ return draft;
43
+ }
44
+ function restoreCommentDraft(draft) {
45
+ if (!draft || draft.comment === null) return;
46
+ var input = panel().querySelector('#issue-comment-input');
47
+ if (input) input.value = draft.comment;
48
+ }
49
+ function canRestoreCommentDraft(draft, ref) {
50
+ return !!draft && draft.ref === ref;
51
+ }
52
+ function hasNewerDraft(item) {
53
+ return (item.intent === 'detail' || item.intent === 'refresh') && item.viewGeneration !== (store.get('issuesViewGeneration') || 0);
54
+ }
55
+ function noteIssueUpdate(ref, revision) {
56
+ var current = store.get('issuesUpdateAvailable');
57
+ if (!current || current.ref !== ref || Number(revision || 0) >= Number(current.revision || 0)) {
58
+ store.set({ issuesUpdateAvailable: { ref: ref, revision: Number(revision || 0) } });
59
+ }
60
+ renderContextualUpdate();
61
+ }
62
+ function refreshSelectedIssue(ref) {
63
+ var selected = store.get('issuesSelected');
64
+ if (!selected || selected.ref !== ref || store.get('issuesView') === 'edit') return;
65
+ store.set({ issuesViewRequest: request('issue_read', { ref: ref }, 'refresh') });
66
+ }
13
67
  function bumpViewGeneration() { store.set({ issuesViewGeneration: (store.get('issuesViewGeneration') || 0) + 1 }); }
14
68
  function request(action, args, intent) {
15
69
  var ws = getWs();
16
70
  if (!ws || ws.readyState !== 1) { showToast('Reconnect to use Issues', 'warn'); return; }
17
71
  var id = 'issues-' + crypto.randomUUID();
18
72
  var pending = Object.assign({}, store.get('issuesRequests') || {});
19
- pending[id] = { action: action, intent: intent, slug: store.get('currentSlug'), sourceRef: entryRef(), view: store.get('issuesView'), viewGeneration: store.get('issuesViewGeneration') || 0 };
73
+ pending[id] = { action: action, intent: intent, slug: store.get('currentSlug'), sourceRef: entryRef(), view: store.get('issuesView'), viewGeneration: store.get('issuesViewGeneration') || 0, draft: captureDraft() };
20
74
  store.set({ issuesRequests: pending });
21
75
  ws.send(JSON.stringify({ type: action, args: args || {}, requestId: id }));
22
76
  return id;
@@ -43,7 +97,9 @@ function detail(entry) {
43
97
  panel().querySelector('.issues-filters').hidden = true;
44
98
  panel().querySelector('[data-issues-back]').classList.remove('hidden');
45
99
  content().innerHTML = renderIssueDetail(entry);
100
+ enhanceClayLogLinks(content());
46
101
  refreshIcons();
102
+ renderContextualUpdate();
47
103
  }
48
104
  function entryRef() {
49
105
  var entry = store.get('issuesSelected');
@@ -81,6 +137,7 @@ function confirmDelete() {
81
137
  store.set({ issuesMutationRequest: request('issue_delete', { ref: entry.ref, expectedRevision: entry.revision }, 'delete') });
82
138
  }
83
139
  export function closeIssues() {
140
+ releaseRightWorkbench("issues");
84
141
  bumpViewGeneration();
85
142
  if (panel()) panel().classList.add('hidden');
86
143
  store.set({ issuesOpen: false });
@@ -90,8 +147,9 @@ export function closeIssues() {
90
147
  export function openIssues(ref) {
91
148
  if (!panel() || store.get('isMate')) return;
92
149
  panel().querySelector('.issues-notice').textContent = '';
150
+ clearUpdateNotice();
93
151
  bumpViewGeneration();
94
- closeProjectLogs(); closeScheduledTasks(); closeNotesBrowser(); closeFileViewer(); closeTerminal();
152
+ claimRightWorkbench("issues");
95
153
  panel().classList.remove('hidden');
96
154
  store.set({ issuesOpen: true });
97
155
  var button = document.getElementById('issues-btn');
@@ -102,9 +160,10 @@ export function openIssues(ref) {
102
160
  export function initIssues() {
103
161
  var host = document.getElementById('main-panels');
104
162
  if (!host || panel()) return;
163
+ registerRightWorkbench("issues", closeIssues);
105
164
  var el = document.createElement('section');
106
165
  el.id = 'issues-panel'; el.className = 'hidden'; el.setAttribute('aria-label', 'Project Issues');
107
- el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option>bug</option><option>feature</option><option>plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
166
+ el.innerHTML = '<header class="issues-topbar"><button class="issues-icon-btn" data-issues-back aria-label="Back to issues" title="Back to issue list"><i data-lucide="arrow-left"></i></button><span class="issues-title"><i data-lucide="circle-dot"></i>Issues</span><span class="issues-subtitle">Manage issues through Driver chat.</span><div class="issues-window-actions"><button class="issues-icon-btn" data-issues-wide aria-label="Widen Issues" title="Widen Issues" aria-pressed="false"><i data-lucide="chevrons-left-right"></i></button><button class="issues-icon-btn" data-issues-full aria-label="Toggle Issues fullscreen" title="Toggle fullscreen" aria-pressed="false"><i data-lucide="maximize-2"></i></button><button class="issues-icon-btn" data-issues-close aria-label="Close Issues" title="Close Issues"><i data-lucide="x"></i></button></div></header><div class="issues-filters"><label class="issues-search"><i data-lucide="search"></i><input name="query" type="search" placeholder="Search issues" aria-label="Search issues"></label><select name="filter-status" aria-label="Filter status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select><select name="filter-type" aria-label="Filter type"><option value="">All types</option><option value="bug">Bug</option><option value="feature">Feature</option><option value="plan">Plan</option></select></div><p class="issues-notice" role="status"></p><main class="issues-content" role="region" aria-live="polite"></main><div class="issues-delete-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="issues-delete-heading"><div class="issues-delete-card"><h2 id="issues-delete-heading">Delete this issue?</h2><p>This removes the issue from the board but preserves its history.</p><p class="issues-delete-title"></p><p class="issues-delete-status" role="status"></p><div><button type="button" data-issue-delete-cancel>Cancel</button><button type="button" data-issue-delete-confirm>Delete issue</button></div></div></div>';
108
167
  host.appendChild(el);
109
168
  var button = document.getElementById('issues-btn');
110
169
  if (button) button.addEventListener('click', function () { if (store.get('issuesOpen')) closeIssues(); else openIssues(); });
@@ -120,6 +179,7 @@ export function initIssues() {
120
179
  else if (target.hasAttribute('data-issue-delete-confirm')) confirmDelete();
121
180
  else if (target.hasAttribute('data-issue-edit')) showIssueEdit(entry);
122
181
  else if (target.hasAttribute('data-issue-cancel')) detail(entry);
182
+ else if (target.dataset.issueRefresh) refreshSelectedIssue(target.dataset.issueRefresh);
123
183
  else if (target.dataset.openIssue) openIssues(target.dataset.openIssue);
124
184
  else if (target.hasAttribute('data-issues-more')) listing(target.dataset.issuesMore);
125
185
  else if (target.hasAttribute('data-issue-history')) { bumpViewGeneration(); store.set({ issuesView: 'history', issuesViewRequest: request('issue_history', { ref: entry.ref }, 'history') }); }
@@ -175,6 +235,7 @@ function showIssueEdit(entry) {
175
235
  }
176
236
  formEl.addEventListener('change', syncFields);
177
237
  syncFields();
238
+ renderContextualUpdate();
178
239
  }
179
240
  export function handleIssuesMessage(msg) {
180
241
  if (msg.type === 'issue_commented') {
@@ -204,8 +265,11 @@ export function handleIssuesMessage(msg) {
204
265
  }
205
266
  if (msg.type === 'issue_updated') {
206
267
  var cache = Object.assign({}, store.get('issuesCache') || {}); cache[msg.ref] = Object.assign({}, cache[msg.ref] || {}, msg, { projectSlug: store.get('currentSlug') }); store.set({ issuesCache: cache });
207
- document.querySelectorAll('[data-issue-ref]').forEach(function (node) { if (node.dataset.issueRef === msg.ref) { node.textContent = statusLabel(msg.status) + ' · ' + msg.title; node.dataset.status = msg.status; } });
208
- if (store.get('issuesOpen')) panel().querySelector('.issues-notice').textContent = 'An issue changed. Back returns to the latest list; reopening an issue loads its latest revision.';
268
+ document.querySelectorAll('[data-issue-ref]').forEach(function (node) { if (node.dataset.issueRef === msg.ref) updateIssueLink(node, msg); });
269
+ var selected = store.get('issuesSelected');
270
+ var view = store.get('issuesView');
271
+ if (store.get('issuesOpen') && selected && selected.ref === msg.ref && (view === 'detail' || view === 'edit')) noteIssueUpdate(msg.ref, msg.revision);
272
+ else if (store.get('issuesOpen') && view === 'list') listing();
209
273
  return true;
210
274
  }
211
275
  if (msg.type !== 'issues_result') return false;
@@ -241,6 +305,16 @@ export function handleIssuesMessage(msg) {
241
305
  return true;
242
306
  }
243
307
  panel().querySelector('.issues-notice').textContent = '';
308
+ if (hasNewerDraft(item)) {
309
+ if (!msg.error && msg.result && msg.result.ref) {
310
+ var refreshedCache = Object.assign({}, store.get('issuesCache') || {});
311
+ refreshedCache[msg.result.ref] = Object.assign({}, msg.result, { canDelete: msg.canDelete === true, projectSlug: store.get('currentSlug') });
312
+ store.set({ issuesCache: refreshedCache });
313
+ if (Number(msg.result.revision || 0) > Number((store.get('issuesSelected') || {}).revision || 0)) noteIssueUpdate(msg.result.ref, msg.result.revision);
314
+ }
315
+ return true;
316
+ }
317
+ if (item.intent !== 'detail' && item.intent !== 'refresh') clearUpdateNotice();
244
318
  if (item.intent === 'work') { closeIssues(); return true; }
245
319
  if (item.intent === 'comment') {
246
320
  var commented = Object.assign({}, msg.result, { canDelete: msg.canDelete === true });
@@ -264,7 +338,15 @@ export function handleIssuesMessage(msg) {
264
338
  status: panel().querySelector('[name="filter-status"]').value, type: panel().querySelector('[name="filter-type"]').value });
265
339
  store.set({ issuesRows: result.issues }); content().innerHTML = renderIssueList(result); refreshIcons();
266
340
  } else if (item.intent === 'history') { content().innerHTML = renderIssueHistory(msg.result); refreshIcons(); }
267
- else if (item.intent === 'revision') { content().innerHTML = '<p class="issues-history-note">Historical revision · use Back to return to current issues</p>' + renderIssueDetail(msg.result); content().querySelectorAll('.issue-actions, [data-issue-session]').forEach(function (node) { node.remove(); }); refreshIcons(); }
268
- else detail(Object.assign({}, msg.result, { canDelete: msg.canDelete === true }));
341
+ else if (item.intent === 'revision') { content().innerHTML = '<p class="issues-history-note">Historical revision · use Back to return to current issues</p>' + renderIssueDetail(msg.result); content().querySelectorAll('.issue-actions, [data-issue-session], .issue-discussion form').forEach(function (node) { node.remove(); }); enhanceClayLogLinks(content()); refreshIcons(); }
342
+ else if (item.intent === 'save') {
343
+ detail(Object.assign({}, msg.result, { canDelete: msg.canDelete === true }));
344
+ } else {
345
+ var available = store.get('issuesUpdateAvailable');
346
+ if (available && available.ref === msg.result.ref && Number(msg.result.revision || 0) >= Number(available.revision || 0)) clearUpdateNotice();
347
+ var refreshed = Object.assign({}, msg.result, { canDelete: msg.canDelete === true });
348
+ detail(refreshed);
349
+ if (canRestoreCommentDraft(item.draft, msg.result.ref) && (item.intent === 'detail' || item.intent === 'refresh')) restoreCommentDraft(item.draft);
350
+ }
269
351
  return true;
270
352
  }
@@ -2,7 +2,7 @@ import { copyToClipboard, escapeHtml } from './utils.js';
2
2
  import { refreshIcons } from './icons.js';
3
3
  import { getMermaidThemeVars } from './theme.js';
4
4
  import { store } from './store.js';
5
- import { enhanceClayLogLinks } from './clay-log-links.js';
5
+ import { enhanceClayLogLinks, renderClayRecordLink } from './clay-log-links.js';
6
6
  import { renderLocalFileLink, enhanceClayFileLinks } from './clay-file-links.js';
7
7
 
8
8
  // Initialize markdown parser
@@ -12,6 +12,8 @@ marked.use({ gfm: true, breaks: false });
12
12
  marked.use({
13
13
  renderer: {
14
14
  link({ href, title, text }) {
15
+ var recordLink = renderClayRecordLink(href, text);
16
+ if (recordLink) return recordLink;
15
17
  var localLink = renderLocalFileLink(href, title, text);
16
18
  if (localLink) return localLink;
17
19
  var isExternal = href && (href.startsWith('http://') || href.startsWith('https://'));
@@ -4,11 +4,9 @@
4
4
  // Same-origin messages only, both directions.
5
5
 
6
6
  import { store } from './store.js';
7
- import { applyContextView, getContextView } from './app-panels.js';
7
+ import { getEffectiveContextView } from './context-view-preference.js';
8
8
  import { forceExternalLinkToNewTab } from './pane-links.js';
9
9
 
10
- var panelOpen = false;
11
-
12
10
  export function reportPaneContext(data) {
13
11
  if (!store.get('paneMode') || window.parent === window) return;
14
12
  window.parent.postMessage({
@@ -38,11 +36,17 @@ export function forwardPaneMarkdownPresentation(message) {
38
36
  return true;
39
37
  }
40
38
 
41
- // Toggle without setContextView: panes share localStorage with the main app,
42
- // so persisting here would silently rewrite the user's main-view preference.
39
+ export function forwardPaneIssueReference(ref, projectSlug) {
40
+ if (!store.get('paneMode') || window.parent === window) return false;
41
+ window.parent.postMessage({ type: "clay-pane-open-issue", ref: ref, projectSlug: projectSlug || store.get('currentSlug') }, window.location.origin);
42
+ return true;
43
+ }
44
+
45
+ // Toggle without setContextView: panes use a transient override and must not
46
+ // rewrite the user's server-persisted main-view preference.
43
47
  function togglePaneContextPanel() {
44
- panelOpen = !panelOpen;
45
- applyContextView(panelOpen ? "panel" : "off");
48
+ var effectiveView = getEffectiveContextView();
49
+ store.set({ contextViewOverride: effectiveView === "panel" ? "off" : "panel" });
46
50
  }
47
51
 
48
52
  function preparePaneLink(event) {
@@ -55,7 +59,9 @@ function preparePaneLink(event) {
55
59
 
56
60
  export function initPaneBridge() {
57
61
  if (!store.get('paneMode')) return;
58
- panelOpen = getContextView() === "panel";
62
+ // Hydration may still replace the persisted mode. Leave the override null
63
+ // so the first header toggle derives from the actual effective view.
64
+ store.set({ contextViewOverride: null });
59
65
  document.addEventListener("click", preparePaneLink, true);
60
66
  document.addEventListener("auxclick", preparePaneLink, true);
61
67
  window.addEventListener("message", function (event) {
@@ -0,0 +1,27 @@
1
+ import { store } from './store.js';
2
+ import { openIssues } from './issues.js';
3
+
4
+ function isIssueReference(ref) {
5
+ return typeof ref === "string" && /^issue:[A-Za-z0-9_-]{24}$/.test(ref);
6
+ }
7
+
8
+ function isPaneSource(host, source) {
9
+ var frames = host.querySelectorAll(".split-pane-frame");
10
+ for (var i = 0; i < frames.length; i++) {
11
+ if (frames[i].contentWindow === source) return true;
12
+ }
13
+ return false;
14
+ }
15
+
16
+ export function handlePaneIssueMessage(event, host) {
17
+ if (!event || event.origin !== window.location.origin || !host) return false;
18
+ var msg = event.data;
19
+ if (!msg || msg.type !== "clay-pane-open-issue") return false;
20
+ if (!isIssueReference(msg.ref) || !isPaneSource(host, event.source)) return true;
21
+
22
+ var issueTarget = store.get('issuesCache') && store.get('issuesCache')[msg.ref];
23
+ var projectSlug = (issueTarget && issueTarget.projectSlug) || msg.projectSlug || store.get('currentSlug');
24
+ if (projectSlug === store.get('currentSlug')) openIssues(msg.ref);
25
+ else window.location.assign('/p/' + encodeURIComponent(projectSlug) + '/?issue=' + encodeURIComponent(msg.ref));
26
+ return true;
27
+ }
@@ -0,0 +1,13 @@
1
+ import { store } from './store.js';
2
+
3
+ export function applyPermissionHints(container, hints) {
4
+ if (!container || !hints) return;
5
+ if (hints.suppressAlwaysAllowRule) {
6
+ var persistent = container.querySelectorAll('.permission-allow-session, .mate-permission-always, .notif-banner-always');
7
+ persistent.forEach(function(button) { button.remove(); });
8
+ }
9
+ if (hints.defaultToNo && !store.get('replayingHistory')) {
10
+ var decline = container.querySelector('.permission-deny, .mate-permission-deny, .notif-banner-deny');
11
+ if (decline) decline.focus({ preventScroll: true });
12
+ }
13
+ }
@@ -1,10 +1,10 @@
1
1
  // User profile module — profile popovers for people and Mates
2
2
  // Stores profile server-side in ~/.clay/profile.json
3
- // Generated identities use deterministic Clay Imprints.
3
+ // Generated identities use deterministic local pixel identicons.
4
4
 
5
5
  import { iconHtml, refreshIcons } from './icons.js';
6
6
  import { setSTTLang } from './stt.js';
7
- import { avatarUrl, mateAvatarUrl, mateMarkUrl } from './avatar.js';
7
+ import { avatarUrl, mateAvatarUrl, mateMarkUrl, userAvatarSeed } from './avatar.js';
8
8
  import { store } from './store.js';
9
9
  import { applyTerminalFont } from './terminal-prefs.js';
10
10
 
@@ -15,7 +15,7 @@ var popoverEl = null;
15
15
  var saveTimer = null;
16
16
 
17
17
  function getAvatarSeed() {
18
- return profile.avatarSeed || 'anonymous';
18
+ return profile.avatarSeed || String(profileUsername || 'anonymous').substring(0, 30);
19
19
  }
20
20
 
21
21
  // --- API ---
@@ -307,14 +307,14 @@ function showPopover() {
307
307
  html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
308
308
  html += '</div>';
309
309
 
310
- // Clay Imprint or custom photo
310
+ // Pixel identicon or custom photo
311
311
  html += '<div class="profile-field">';
312
312
  html += '<label class="profile-field-label">Identity</label>';
313
313
  html += '<div class="profile-imprint-panel">';
314
- html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated Clay Imprint"><span><strong>Clay Imprint</strong><small>Unique to this identity</small></span></div>';
314
+ html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated pixel avatar"><span><strong>Pixel avatar</strong><small>Unique to this identity</small></span></div>';
315
315
  html += '<div class="profile-imprint-actions">';
316
- html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use imprint</button>';
317
- html += '<button class="profile-regenerate-imprint" type="button" title="Generate another imprint">' + iconHtml('shuffle') + ' New imprint</button>';
316
+ html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use pixel avatar</button>';
317
+ html += '<button class="profile-regenerate-imprint" type="button" title="Generate another pixel avatar">' + iconHtml('shuffle') + ' New avatar</button>';
318
318
  html += '<button class="profile-avatar-upload' + (profile.avatarCustom ? ' is-active' : '') + '" type="button">Upload photo</button>';
319
319
  html += '</div>';
320
320
  html += '</div>';
@@ -511,9 +511,9 @@ export function initProfile(_ctx) {
511
511
  if (data.avatarCustom) profile.avatarCustom = data.avatarCustom;
512
512
  if (data.username) profileUsername = data.username;
513
513
 
514
- // Auto-generate seed if none exists
514
+ // Persist a stable account identity only when the server has not saved a seed.
515
515
  if (!profile.avatarSeed) {
516
- profile.avatarSeed = Math.random().toString(36).substring(2, 10);
516
+ profile.avatarSeed = userAvatarSeed(data);
517
517
  saveProfile();
518
518
  }
519
519
 
@@ -43,3 +43,52 @@ export function isProjectContextConnected(state, slug, socket) {
43
43
  && state.connected === true
44
44
  && state.activeProjectSlug === slug;
45
45
  }
46
+
47
+ export function isProjectSessionReady(state, socket) {
48
+ var target = projectWsPath(state.currentSlug);
49
+ return !!socket
50
+ && socket.readyState === 1
51
+ && state.connected === true
52
+ && !!state.currentSlug
53
+ && state.wsPath === target
54
+ && state.socketPath === target
55
+ && state.activeProjectSlug === state.currentSlug
56
+ && state.sessionActivatedProjectSlug === state.currentSlug
57
+ && state.sessionListProjectSlug === state.currentSlug
58
+ && state.splitGroupsProjectSlug === state.currentSlug;
59
+ }
60
+
61
+ export function splitPanesMatchProject(split, slug) {
62
+ if (!split || !Array.isArray(split.panes) || split.panes.length !== 2 || !slug) return false;
63
+ return split.panes[0].slug === slug && split.panes[1].slug === slug;
64
+ }
65
+
66
+ export function hasCurrentSplitGroup(split, groups) {
67
+ if (!split || !split.groupId) return true;
68
+ var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
69
+ if (!members || members.length !== 2) return false;
70
+ var available = Array.isArray(groups) ? groups : [];
71
+ for (var i = 0; i < available.length; i++) {
72
+ var group = available[i];
73
+ if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !== 2) continue;
74
+ if (group.members.indexOf(members[0]) !== -1 && group.members.indexOf(members[1]) !== -1) return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ export function reconcileRestoredSplit(split, groups) {
80
+ if (!split || !split.groupId) return { action: "none" };
81
+ var available = Array.isArray(groups) ? groups : [];
82
+ for (var i = 0; i < available.length; i++) {
83
+ var group = available[i];
84
+ if (group.id !== split.groupId) continue;
85
+ var oldMembers = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
86
+ var members = Array.isArray(group.members) ? group.members : [];
87
+ if (oldMembers && oldMembers.length === 2 && members.length === 2
88
+ && members.indexOf(oldMembers[0]) !== -1 && members.indexOf(oldMembers[1]) !== -1) {
89
+ return { action: "keep", group: group };
90
+ }
91
+ return { action: "rebuild", group: group };
92
+ }
93
+ return { action: "clear" };
94
+ }
@@ -43,8 +43,9 @@ function actorLabel(actor) {
43
43
  // object (userId, sessionKey, vendor), but a session-authored entry must not
44
44
  // read as if the human owner wrote it: the Project Driver did. The owner is
45
45
  // only the account the session ran under.
46
- function authorLine(entry) {
46
+ export function authorLine(entry) {
47
47
  var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
48
+ if (actor.type === "system") return actor.displayName || "Scheduled Tasks";
48
49
  if (actor.type === "session") {
49
50
  return actor.vendor ? "Project Driver (" + vendorLabel(actor.vendor) + ")" : "Project Driver";
50
51
  }
@@ -62,7 +63,7 @@ function formatTime(value) {
62
63
  catch (e) { return ""; }
63
64
  }
64
65
 
65
- function relativeTime(value) {
66
+ export function relativeTime(value) {
66
67
  if (!value) return "";
67
68
  var delta = Date.now() - value;
68
69
  if (delta < 0) return formatTime(value);
@@ -363,7 +364,7 @@ export function renderDetail(detailEl, entry, handlers) {
363
364
  formatTime(entry.updatedAt || entry.createdAt),
364
365
  ].filter(Boolean).join(" · ");
365
366
  setText(header, ".project-log-byline", byline);
366
- if (handlers && handlers.canDelete) {
367
+ if (handlers && handlers.canDelete && !entry.scheduledResult) {
367
368
  var deleteButton = document.createElement("button");
368
369
  deleteButton.type = "button";
369
370
  deleteButton.className = "project-log-delete-button";
@@ -373,6 +374,15 @@ export function renderDetail(detailEl, entry, handlers) {
373
374
  }
374
375
  article.appendChild(header);
375
376
 
377
+ if (entry.scheduledResult && entry.scheduledResult.driverOriginId && handlers && handlers.onOpenSession) {
378
+ var sessionButton = document.createElement("button");
379
+ sessionButton.type = "button";
380
+ sessionButton.className = "project-log-session-button";
381
+ sessionButton.innerHTML = '<i data-lucide="external-link"></i><span>View session</span>';
382
+ sessionButton.addEventListener("click", function () { handlers.onOpenSession(entry); });
383
+ article.appendChild(sessionButton);
384
+ }
385
+
376
386
  var markdown = document.createElement("div");
377
387
  markdown.className = "project-log-markdown md-content";
378
388
  markdown.innerHTML = renderMarkdown(entry.body || "");
@@ -1,5 +1,4 @@
1
1
  // Project Logs — the project's ledger of agent-authored decisions and work.
2
- //
3
2
  // A bounded right-side workbench pane, like the document viewer and terminal.
4
3
  // Inside it there is one navigation stack rather than a master/detail split:
5
4
  // the list fills the pane, opening an entry replaces it with the full-pane
@@ -14,10 +13,10 @@ import { store } from './store.js';
14
13
  import { getWs } from './ws-ref.js';
15
14
  import { refreshIcons } from './icons.js';
16
15
  import { showToast } from './utils.js';
17
- import { closeScheduledTasks } from './scheduled-tasks.js';
18
- import { closeNotesBrowser } from './sticky-notes-browser.js';
19
- import { closeFileViewer } from './filebrowser.js';
20
- import { closeTerminal } from './terminal.js';
16
+ import { showScheduledResultNotification } from './scheduled-result-notification.js';
17
+ import * as scheduledResultClient from './scheduled-result-client.js';
18
+ import { clearPendingScheduledResultRead, handleScheduledResultDisplayed, initScheduledResultDisplay, openScheduledResultSession } from './scheduled-result-display.js';
19
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
21
20
  import { renderFilter, renderContextFilter, renderList, renderDetail } from './project-logs-render.js';
22
21
 
23
22
  var panel = null;
@@ -35,7 +34,7 @@ var requestCounter = 0;
35
34
  function syncUnreadBadge() {
36
35
  var button = document.getElementById("project-logs-btn");
37
36
  if (!button) return;
38
- var unread = store.get('projectLogsUnread') || 0;
37
+ var unread = (store.get('projectLogsUnread') || 0) + (store.get('scheduledResultUnread') || 0);
39
38
  button.classList.toggle("project-logs-unread", unread > 0);
40
39
  var badge = document.getElementById("project-logs-count");
41
40
  if (!badge) return;
@@ -164,10 +163,10 @@ function applyWindowState(wide, fullscreen) {
164
163
  }
165
164
 
166
165
  // --- Navigation stack ----------------------------------------------------
167
-
168
166
  function showList() {
169
167
  if (!panel) return;
170
168
  store.set({ projectLogsView: "list", projectLogsSelectedRef: null });
169
+ clearPendingScheduledResultRead();
171
170
  detailEl.classList.add("hidden");
172
171
  listEl.classList.remove("hidden");
173
172
  panel.querySelector("#project-logs-toolbar").classList.remove("hidden");
@@ -188,8 +187,10 @@ function showDetail(entry) {
188
187
  renderDetail(detailEl, entry, {
189
188
  onComment: submitComment,
190
189
  onDelete: openDeleteDialog,
190
+ onOpenSession: openScheduledResultSession,
191
191
  canDelete: store.get('projectLogsCanDelete') === true,
192
192
  });
193
+ handleScheduledResultDisplayed(entry);
193
194
  }
194
195
 
195
196
  function openDeleteDialog(entry) {
@@ -230,7 +231,6 @@ function confirmDelete() {
230
231
  }
231
232
 
232
233
  // --- Requests ------------------------------------------------------------
233
-
234
234
  function requestList() {
235
235
  var requestId = nextRequestId("list-logs");
236
236
  store.set({ projectLogsListRequestId: requestId });
@@ -282,14 +282,17 @@ function applyContextFilter(contextMode) {
282
282
  // --- Lifecycle -----------------------------------------------------------
283
283
 
284
284
  export function initProjectLogs() {
285
+ registerRightWorkbench("project-logs", closeProjectLogs);
285
286
  var button = document.getElementById("project-logs-btn");
286
287
  if (!button) return;
287
288
  button.addEventListener("click", function () {
288
289
  if (store.get('projectLogsOpen')) closeProjectLogs();
289
290
  else openProjectLogs();
290
291
  });
292
+ initScheduledResultDisplay();
291
293
  syncUnreadBadge();
292
294
  store.subscribe(function (state, previous) {
295
+ if (state.connected === false && previous.connected === true) store.set({ scheduledResultAutoFocusHandled: false });
293
296
  if (state.currentSlug === previous.currentSlug) return;
294
297
  closeProjectLogs();
295
298
  applyWindowState(false, false);
@@ -309,7 +312,13 @@ export function initProjectLogs() {
309
312
  projectLogsDeleteRequestId: null,
310
313
  projectLogsCanDelete: false,
311
314
  projectLogsUnread: 0,
312
- projectLogsSeenRevisions: {}
315
+ projectLogsSeenRevisions: {},
316
+ scheduledResultUnread: 0,
317
+ scheduledResultAutoFocusHandled: false,
318
+ scheduledResultFocusRef: null,
319
+ scheduledResultPendingReadRef: null,
320
+ scheduledResultSeenRefs: {},
321
+ scheduledResultSessionRequestId: null,
313
322
  });
314
323
  syncUnreadBadge();
315
324
  });
@@ -320,11 +329,7 @@ export function initProjectLogs() {
320
329
  export function openProjectLogs() {
321
330
  ensurePanel();
322
331
  if (!panel) return;
323
- closeScheduledTasks();
324
- closeNotesBrowser();
325
- // Claim the single right workbench slot.
326
- try { closeFileViewer(); } catch (e) {}
327
- try { closeTerminal(); } catch (e) {}
332
+ claimRightWorkbench("project-logs");
328
333
  panel.classList.remove("hidden");
329
334
  applyWindowState(store.get('projectLogsWide'), false);
330
335
  var button = document.getElementById("project-logs-btn");
@@ -344,6 +349,7 @@ export function openProjectLog(ref) {
344
349
  }
345
350
 
346
351
  export function closeProjectLogs() {
352
+ releaseRightWorkbench("project-logs");
347
353
  if (!store.get('projectLogsOpen')) return;
348
354
  if (panel) {
349
355
  panel.classList.add("hidden");
@@ -384,11 +390,35 @@ export function handleProjectLogsState(msg) {
384
390
 
385
391
  export function handleProjectLogEntry(msg) {
386
392
  if (msg.requestId && msg.requestId !== store.get('projectLogsReadRequestId')) return;
387
- if (!msg.entry) return;
393
+ if (!msg.entry || !store.get('projectLogsOpen')) return;
388
394
  store.set({ projectLogsCanDelete: msg.canDelete === true });
389
395
  showDetail(msg.entry);
390
396
  }
391
397
 
398
+ export function handleScheduledTaskResultsState(msg) {
399
+ var transition = scheduledResultClient.scheduledResultStateTransition(store.snap(), msg);
400
+ store.set(transition.patch); syncUnreadBadge();
401
+ if (!transition.openRef) return false;
402
+ var opened = openProjectLog(transition.openRef);
403
+ if (!opened) store.set({ scheduledResultFocusRef: null });
404
+ return opened;
405
+ }
406
+
407
+ export function handleScheduledTaskResultCreated(msg) {
408
+ var patch = scheduledResultClient.scheduledResultCreatedTransition(store.snap(), msg);
409
+ if (!patch) return false;
410
+ store.set(patch); syncUnreadBadge();
411
+ showScheduledResultNotification(msg.title || "Scheduled task finished", msg.summary || "", function () { openProjectLog(msg.ref); });
412
+ return true;
413
+ }
414
+
415
+ export function handleScheduledTaskResultSession(msg) {
416
+ var sessionId = scheduledResultClient.scheduledResultSessionId(store.snap(), msg);
417
+ if (sessionId === null) return false;
418
+ store.set({ scheduledResultSessionRequestId: null });
419
+ return send({ type: "switch_session", id: sessionId });
420
+ }
421
+
392
422
  export function handleProjectLogCommented(msg) {
393
423
  if (msg.requestId && msg.requestId !== store.get('projectLogsCommentRequestId')) return;
394
424
  if (!msg.entry) return;