clay-server 4.1.0-beta.9 → 4.1.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/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-scheduled-result-store.js +131 -0
  20. package/lib/project-scheduled-task-execution.js +35 -2
  21. package/lib/project-session-notes.js +11 -4
  22. package/lib/project-sessions.js +7 -0
  23. package/lib/project-user-message.js +47 -0
  24. package/lib/project.js +53 -3
  25. package/lib/public/app.js +26 -9
  26. package/lib/public/css/avatar-imprints.css +1 -1
  27. package/lib/public/css/command-palette.css +20 -0
  28. package/lib/public/css/git-panel.css +13 -12
  29. package/lib/public/css/git-placard.css +4 -4
  30. package/lib/public/css/issues.css +78 -31
  31. package/lib/public/css/mobile-nav.css +7 -0
  32. package/lib/public/css/notifications-center.css +3 -3
  33. package/lib/public/css/project-logs.css +15 -0
  34. package/lib/public/css/sidebar.css +7 -0
  35. package/lib/public/css/sticky-notes.css +29 -0
  36. package/lib/public/modules/app-connection.js +9 -1
  37. package/lib/public/modules/app-favicon.js +10 -0
  38. package/lib/public/modules/app-messages.js +43 -12
  39. package/lib/public/modules/app-notifications.js +2 -0
  40. package/lib/public/modules/app-panels.js +77 -23
  41. package/lib/public/modules/app-projects.js +2 -0
  42. package/lib/public/modules/app-rate-limit.js +148 -61
  43. package/lib/public/modules/avatar-imprint.js +25 -70
  44. package/lib/public/modules/avatar.js +12 -4
  45. package/lib/public/modules/chat-render-runtime.js +20 -0
  46. package/lib/public/modules/clay-issue-links.js +23 -2
  47. package/lib/public/modules/clay-log-links.js +11 -1
  48. package/lib/public/modules/context-view-preference.js +143 -0
  49. package/lib/public/modules/default-vendor.js +116 -0
  50. package/lib/public/modules/filebrowser-tabs.js +1 -0
  51. package/lib/public/modules/filebrowser.js +45 -18
  52. package/lib/public/modules/issues-render.js +36 -10
  53. package/lib/public/modules/issues.js +95 -13
  54. package/lib/public/modules/markdown.js +3 -1
  55. package/lib/public/modules/pane-bridge.js +14 -8
  56. package/lib/public/modules/pane-issue-bridge.js +27 -0
  57. package/lib/public/modules/permission-hints.js +13 -0
  58. package/lib/public/modules/profile.js +9 -9
  59. package/lib/public/modules/project-activation.js +49 -0
  60. package/lib/public/modules/project-logs-render.js +13 -3
  61. package/lib/public/modules/project-logs.js +45 -15
  62. package/lib/public/modules/right-workbench.js +44 -0
  63. package/lib/public/modules/scheduled-result-client.js +33 -0
  64. package/lib/public/modules/scheduled-result-display.js +50 -0
  65. package/lib/public/modules/scheduled-result-notification.js +31 -0
  66. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  67. package/lib/public/modules/scheduled-tasks.js +9 -21
  68. package/lib/public/modules/sidebar-mobile.js +51 -3
  69. package/lib/public/modules/sidebar-sessions.js +65 -5
  70. package/lib/public/modules/split-session-boundary.js +28 -0
  71. package/lib/public/modules/split-view.js +42 -7
  72. package/lib/public/modules/sticky-notes-browser.js +130 -18
  73. package/lib/public/modules/sticky-notes.js +2 -3
  74. package/lib/public/modules/terminal.js +4 -4
  75. package/lib/public/modules/thinking-lifecycle.js +4 -1
  76. package/lib/public/modules/tools.js +5 -1
  77. package/lib/scheduled-run-state.js +10 -2
  78. package/lib/scheduler.js +1 -0
  79. package/lib/sdk-bridge.js +97 -61
  80. package/lib/sdk-message-processor.js +44 -13
  81. package/lib/sdk-skill-discovery.js +6 -28
  82. package/lib/sdk-turn-queue.js +14 -0
  83. package/lib/server-context-view.js +53 -0
  84. package/lib/server-default-vendor.js +60 -0
  85. package/lib/server.js +23 -6
  86. package/lib/session-notes-mcp-server.js +3 -5
  87. package/lib/session-pair-factory.js +2 -2
  88. package/lib/session-title-generator.js +89 -0
  89. package/lib/session-title-policy.js +8 -2
  90. package/lib/sessions.js +26 -1
  91. package/lib/user-presence.js +6 -0
  92. package/lib/users-context-view-preferences.js +65 -0
  93. package/lib/users-default-vendor-preferences.js +80 -0
  94. package/lib/users.js +12 -0
  95. package/lib/ws-schema.js +11 -0
  96. package/lib/yoke/acp-agent-profiles.js +15 -2
  97. package/lib/yoke/adapters/acp.js +3 -2
  98. package/lib/yoke/adapters/antigravity.js +2 -2
  99. package/lib/yoke/adapters/claude-worker.js +11 -12
  100. package/lib/yoke/adapters/claude.js +75 -22
  101. package/lib/yoke/adapters/codex.js +216 -29
  102. package/lib/yoke/adapters/kiro.js +6 -5
  103. package/lib/yoke/claude-context-usage.js +57 -0
  104. package/lib/yoke/claude-message-correlation.js +31 -0
  105. package/lib/yoke/claude-model-switch.js +15 -0
  106. package/lib/yoke/index.js +5 -1
  107. package/lib/yoke/mcp-bridge-server.js +18 -5
  108. package/lib/yoke/session-skill-catalog.js +45 -0
  109. package/lib/yoke/skill-discovery.js +60 -20
  110. package/lib/yoke/vendor-registry.js +1 -1
  111. package/package.json +5 -7
@@ -2,15 +2,14 @@ import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { escapeHtml, copyToClipboard } from './utils.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, exportMarkdownAsPdf } from './markdown.js';
4
4
  import { closeSidebar } from './sidebar.js';
5
- import { closeTerminal } from './terminal.js';
5
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench, isRightWorkbenchCurrent } from './right-workbench.js';
6
6
  import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
7
7
  import { initFileIcons, getFileIconSvg, getFolderIconSvg } from './fileicons.js';
8
8
  import { copyMarkdownFormatting } from './rich-clipboard.js';
9
9
  import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow, isFollowingMarkdown } from './markdown-live-edit.js';
10
10
  import { store } from './store.js';
11
11
  import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
12
- import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
13
- import { closeIssues } from './issues.js';
12
+ import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, focusedFileViewerTabData, clearFileViewerTabs } from './filebrowser-tabs.js';
14
13
  import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
15
14
  import { registerClayFileLinkOpener } from './clay-file-links.js';
16
15
 
@@ -41,10 +40,13 @@ export function initFileBrowser(_ctx) {
41
40
  if (mainPanels && ctx.fileViewerEl) mainPanels.appendChild(ctx.fileViewerEl);
42
41
  initFileViewerTabs({
43
42
  onFocus: function(path) {
43
+ claimRightWorkbench("file-viewer");
44
+ ctx.fileViewerEl.classList.remove("hidden");
44
45
  if (path !== currentFilePath) requestFileContent(path);
45
46
  },
46
47
  onEmpty: function() { teardownFileViewer(); },
47
48
  });
49
+ registerRightWorkbench("file-viewer", teardownFileViewer);
48
50
 
49
51
  // Load material file icons in background
50
52
  initFileIcons().then(function () {
@@ -149,7 +151,7 @@ export function initFileBrowser(_ctx) {
149
151
 
150
152
  // Close button
151
153
  document.getElementById("file-viewer-close").addEventListener("click", function () {
152
- closeFileViewerTab(focusedFileViewerTab());
154
+ closeFileViewer();
153
155
  });
154
156
 
155
157
  // Full-viewport presentation toggle
@@ -395,6 +397,7 @@ function kbCollapseOrAscend() {
395
397
  function kbActivate() {
396
398
  if (!_kbFocused) return;
397
399
  if (!isDirRow(_kbFocused) && _kbFocused.dataset.path) {
400
+ claimRightWorkbench("file-viewer");
398
401
  openFileViewerTab(_kbFocused.dataset.path);
399
402
  }
400
403
  _kbFocused.click();
@@ -477,10 +480,7 @@ function ensureRenderedMarkdown() {
477
480
  }
478
481
 
479
482
  export function closeFileViewer() {
480
- if (focusedFileViewerTab()) {
481
- closeFileViewerTab(focusedFileViewerTab());
482
- return;
483
- }
483
+ releaseRightWorkbench("file-viewer");
484
484
  teardownFileViewer();
485
485
  }
486
486
 
@@ -512,6 +512,7 @@ export function setFileViewerFullscreen(enabled) {
512
512
  }
513
513
 
514
514
  export function resetFileBrowser() {
515
+ releaseRightWorkbench("file-viewer");
515
516
  clearFileViewerTabs();
516
517
  teardownFileViewer();
517
518
  // Clear all cached state
@@ -552,12 +553,13 @@ export function openFile(filePath, opts) {
552
553
  if (!state.connected) return false;
553
554
  if (opts && opts.projectSlug && opts.projectSlug !== state.currentSlug) return false;
554
555
  if (opts && opts.sessionId && String(opts.sessionId) !== String(state.activeSessionId)) return false;
555
- closeIssues();
556
+ var workbenchRevision = claimRightWorkbench("file-viewer");
556
557
  if (followedFileChanged(filePath) || (opts && opts.diff && isFollowingMarkdown(filePath))) {
557
558
  cancelMarkdownFollow();
558
559
  }
559
560
  pendingRenderedOpen = !!(opts && opts.rendered);
560
561
  store.set({ pendingFileNavigation: opts && opts.line ? { path: filePath, line: opts.line, column: opts.column || null, requestId: null } : null });
562
+ store.set({ fileViewerOpenRevision: workbenchRevision });
561
563
  openFileViewerTab(filePath);
562
564
  if (opts && opts.diff) {
563
565
  pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
@@ -567,9 +569,28 @@ export function openFile(filePath, opts) {
567
569
  requestFileContent(filePath);
568
570
  }
569
571
 
572
+ export function reopenFileViewer() {
573
+ var workbenchRevision = claimRightWorkbench("file-viewer");
574
+ store.set({ fileViewerOpenRevision: workbenchRevision });
575
+ if (!focusedFileViewerTab()) return false;
576
+ ctx.fileViewerEl.classList.remove("hidden");
577
+ var tab = focusedFileViewerTabData();
578
+ if (currentFilePath === focusedFileViewerTab() && currentContent != null) {
579
+ sendWatch(currentFilePath);
580
+ return true;
581
+ }
582
+ if (tab && tab.content != null) {
583
+ showFileContent({ path: focusedFileViewerTab(), content: tab.content, size: tab.content.length });
584
+ return true;
585
+ }
586
+ requestFileContent(focusedFileViewerTab());
587
+ return true;
588
+ }
589
+
570
590
  export function openWorkingTreeDiff(diff) {
571
591
  if (!diff || !diff.path) return;
572
- closeIssues();
592
+ var workbenchRevision = claimRightWorkbench("file-viewer");
593
+ store.set({ fileViewerOpenRevision: workbenchRevision });
573
594
  openFileViewerTab(diff.path);
574
595
  pendingRenderedOpen = false;
575
596
  pendingOpenMode = diff.binary ? null : {
@@ -588,6 +609,8 @@ export function openWorkingTreeDiff(diff) {
588
609
 
589
610
  export function presentMarkdownEdit(msg) {
590
611
  if (!msg || !beginMarkdownPresentation(msg.path)) return;
612
+ var workbenchRevision = claimRightWorkbench("file-viewer");
613
+ store.set({ fileViewerOpenRevision: workbenchRevision });
591
614
  openFileViewerTab(msg.path, { content: msg.content });
592
615
  pendingOpenMode = null;
593
616
  pendingRenderedOpen = true;
@@ -687,7 +710,7 @@ function requestDirectory(dirPath) {
687
710
  function requestFileContent(filePath) {
688
711
  var requestId = "file-read-" + Date.now() + "-" + Math.random().toString(36).slice(2);
689
712
  var state = store.snap();
690
- store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null } });
713
+ store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null, workbenchRevision: store.get('rightWorkbenchRevision') || 0 } });
691
714
  var pending = store.get('pendingFileNavigation');
692
715
  if (pending && pending.path === filePath) store.set({ pendingFileNavigation: Object.assign({}, pending, { requestId: requestId }) });
693
716
  if (ctx.ws && ctx.connected) {
@@ -811,6 +834,7 @@ function renderFilteredTree(container, tree, depth, query) {
811
834
  (function (rowEl, childEl, folderName) {
812
835
  rowEl.addEventListener("click", function (e) {
813
836
  e.stopPropagation();
837
+ claimRightWorkbench("file-viewer");
814
838
  var isExpanded = rowEl.classList.contains("expanded");
815
839
  rowEl.classList.toggle("expanded");
816
840
  childEl.classList.toggle("hidden", isExpanded);
@@ -833,6 +857,7 @@ function renderFilteredTree(container, tree, depth, query) {
833
857
  (function (filePath, rowEl) {
834
858
  rowEl.addEventListener("click", function (e) {
835
859
  e.stopPropagation();
860
+ claimRightWorkbench("file-viewer");
836
861
  if (followedFileChanged(filePath)) cancelMarkdownFollow();
837
862
  var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
838
863
  if (prev) prev.classList.remove("active");
@@ -843,7 +868,7 @@ function renderFilteredTree(container, tree, depth, query) {
843
868
  });
844
869
  rowEl.addEventListener("dblclick", function (e) {
845
870
  e.stopPropagation();
846
- openFileViewerTab(filePath);
871
+ openFile(filePath);
847
872
  });
848
873
  })(entry.path, row);
849
874
 
@@ -973,7 +998,7 @@ export function handleFsRead(msg) {
973
998
  if ((request.projectSlug || null) !== (msg.projectSlug || null)) return;
974
999
  if ((request.sessionId || null) !== (msg.sessionId == null ? null : String(msg.sessionId))) return;
975
1000
  if ((request.accountId || null) !== (msg.accountId || null)) return;
976
- showFileContent(msg);
1001
+ showFileContent(msg, request.workbenchRevision);
977
1002
  }
978
1003
 
979
1004
  // --- Tree rendering ---
@@ -1040,6 +1065,7 @@ function renderEntries(container, entries, depth) {
1040
1065
  (function (dirPath, childEl, rowEl, folderName) {
1041
1066
  rowEl.addEventListener("click", function (e) {
1042
1067
  e.stopPropagation();
1068
+ claimRightWorkbench("file-viewer");
1043
1069
  var isExpanded = rowEl.classList.contains("expanded");
1044
1070
  if (isExpanded) {
1045
1071
  rowEl.classList.remove("expanded");
@@ -1079,6 +1105,7 @@ function renderEntries(container, entries, depth) {
1079
1105
  (function (filePath, rowEl) {
1080
1106
  rowEl.addEventListener("click", function (e) {
1081
1107
  e.stopPropagation();
1108
+ claimRightWorkbench("file-viewer");
1082
1109
  if (followedFileChanged(filePath)) cancelMarkdownFollow();
1083
1110
  // Mark active
1084
1111
  var prev = ctx.fileTreeEl.querySelector(".file-tree-item.active");
@@ -1093,7 +1120,7 @@ function renderEntries(container, entries, depth) {
1093
1120
  });
1094
1121
  rowEl.addEventListener("dblclick", function (e) {
1095
1122
  e.stopPropagation();
1096
- openFileViewerTab(filePath);
1123
+ openFile(filePath);
1097
1124
  });
1098
1125
  })(entry.path, row);
1099
1126
 
@@ -1105,8 +1132,9 @@ function renderEntries(container, entries, depth) {
1105
1132
 
1106
1133
  // --- File viewer ---
1107
1134
 
1108
- function showFileContent(msg) {
1135
+ function showFileContent(msg, expectedRevision) {
1109
1136
  if (!updateFileViewerTab(msg.path, { content: msg.content })) return;
1137
+ if (expectedRevision != null && !isRightWorkbenchCurrent("file-viewer", expectedRevision)) return;
1110
1138
  var pathEl = document.getElementById("file-viewer-path");
1111
1139
  var bodyEl = document.getElementById("file-viewer-body");
1112
1140
  var renderBtn = document.getElementById("file-viewer-render");
@@ -1183,7 +1211,6 @@ function showFileContent(msg) {
1183
1211
  }
1184
1212
  }
1185
1213
 
1186
- closeTerminal();
1187
1214
  ctx.fileViewerEl.classList.remove("hidden");
1188
1215
  sendWatch(msg.path);
1189
1216
  refreshIcons();
@@ -1291,7 +1318,7 @@ function pathsReferToSameFile(left, right) {
1291
1318
  }
1292
1319
 
1293
1320
  export function handleFileChanged(msg) {
1294
- if (!msg.path || msg.path !== currentFilePath) return;
1321
+ if (!msg.path || !pathsReferToSameFile(msg.path, currentFilePath)) return;
1295
1322
  if (ctx.fileViewerEl.classList.contains("hidden")) return;
1296
1323
  if (historyVisible || inlineDiffActive) return;
1297
1324
  if (msg.content === currentContent) return;
@@ -1299,7 +1326,7 @@ export function handleFileChanged(msg) {
1299
1326
  var bodyEl = document.getElementById("file-viewer-body");
1300
1327
  var scrollPos = bodyEl ? bodyEl.scrollTop : 0;
1301
1328
  pendingRefresh = true;
1302
- showFileContent(msg);
1329
+ showFileContent(msg.path === currentFilePath ? msg : Object.assign({}, msg, { path: currentFilePath }));
1303
1330
  if (bodyEl) bodyEl.scrollTop = scrollPos;
1304
1331
  }
1305
1332
 
@@ -1,24 +1,50 @@
1
1
  import { escapeHtml } from './utils.js';
2
2
  import { renderMarkdown } from './markdown.js';
3
+ import { authorLine, relativeTime } from './project-logs-render.js';
3
4
  export function statusLabel(status) { return { open: 'Open', in_progress: 'In progress', resolved: 'Resolved', closed: 'Closed' }[status] || 'Issue'; }
4
5
  function esc(value) { return escapeHtml(String(value || '')); }
5
6
  function options(values, selected) { return values.map(function (value) { return '<option value="' + value + '"' + (value === selected ? ' selected' : '') + '>' + esc(statusLabel(value) === 'Issue' ? value.replace(/_/g, ' ') : statusLabel(value)) + '</option>'; }).join(''); }
7
+ function label(value) { return value ? value.charAt(0).toUpperCase() + value.slice(1).replace(/_/g, ' ') : ''; }
8
+ function statusChip(status) { return '<span class="issue-status" data-status="' + esc(status) + '">' + statusLabel(status) + '</span>'; }
9
+ function priorityChip(priority) { return priority && priority !== 'normal' ? '<span class="project-log-priority" data-priority="' + esc(priority) + '">' + esc(label(priority)) + '</span>' : ''; }
10
+ function action(attribute, icon, text, extra) { return '<button type="button" ' + attribute + (extra || '') + '><i data-lucide="' + icon + '"></i>' + text + '</button>'; }
11
+ function prose(text, className) { return '<div class="markdown-body ' + (className || '') + '">' + renderMarkdown(text || '') + '</div>'; }
6
12
  export function renderIssueList(result) {
7
13
  var filtered = !!(result.query || result.status || result.type);
8
14
  if (!result.issues.length) return '<div class="issues-empty"><i data-lucide="circle-dot"></i><h2>' + (filtered ? 'No matching issues' : 'No issues yet') + '</h2><p>' + (filtered ? 'Try a different search or filter.' : 'Manage issues through Driver chat.') + '</p></div>';
9
15
  return result.issues.map(function (entry) {
10
- return '<button class="issue-row" data-open-issue="' + esc(entry.ref) + '"><div class="issue-row-head"><span class="issue-type">' + esc(entry.type) + '</span><span class="issue-status" data-status="' + entry.status + '">' + statusLabel(entry.status) + '</span></div><strong>' + esc(entry.title) + '</strong><p>' + esc(entry.summary) + '</p><small>' + esc(entry.priority) + ' · revision ' + entry.revision + '</small></button>';
11
- }).join('') + (result.nextCursor ? '<button data-issues-more="' + esc(result.nextCursor) + '">Load more</button>' : '');
16
+ var meta = [authorLine(entry), relativeTime(entry.updatedAt || entry.createdAt)];
17
+ if (entry.revision > 1) meta.push('v' + entry.revision);
18
+ if (entry.commentCount) meta.push(entry.commentCount + (entry.commentCount === 1 ? ' comment' : ' comments'));
19
+ return '<button type="button" class="issue-row" data-open-issue="' + esc(entry.ref) + '">' +
20
+ '<div class="issue-row-head"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + '<strong>' + esc(entry.title) + '</strong></div>' +
21
+ (entry.summary ? '<p>' + esc(entry.summary) + '</p>' : '') +
22
+ '<div class="issue-row-meta">' + statusChip(entry.status) + '<span>' + esc(meta.filter(Boolean).join(' · ')) + '</span></div></button>';
23
+ }).join('') + (result.nextCursor ? '<div class="issues-load-more"><button data-issues-more="' + esc(result.nextCursor) + '">Load more</button></div>' : '');
24
+ }
25
+ function renderComment(comment) {
26
+ var actor = comment.author || {};
27
+ var review = comment.review;
28
+ return '<article class="issue-comment"><div class="issue-comment-meta">' + esc(actor.displayName || actor.userId || 'Project member') + ' · ' + esc(new Date(comment.at).toLocaleString()) + '</div>' +
29
+ prose(comment.body, 'issue-comment-body') + (review ? '<div class="issue-comment-review"><span>' + esc(label(review.action)) + '</span>' + prose(review.response) + '</div>' : '') + '</article>';
12
30
  }
13
31
  export function renderIssueDetail(entry) {
14
- var author = entry.updatedBy || entry.author || {};
15
- var comments = (entry.comments || []).map(function (comment) { var commentAuthor = comment.author || {}; var review = comment.review; return '<article class="issue-comment"><div class="issue-comment-meta">' + esc(commentAuthor.displayName || commentAuthor.userId || 'Project member') + ' · ' + esc(new Date(comment.at).toLocaleString()) + '</div><p>' + esc(comment.body) + '</p>' + (review ? '<small>' + esc(review.action) + ' · ' + esc(review.response) + '</small>' : '') + '</article>'; }).join('');
16
- return '<div class="issue-detail"><span class="issue-status" data-status="' + entry.status + '">' + statusLabel(entry.status) + '</span><h2>' + esc(entry.title) + '</h2><p>' + esc(entry.summary) + '</p><small>' + esc(entry.type) + ' · ' + esc(entry.priority) + ' · revision ' + entry.revision + ' · ' + esc(author.displayName || author.userId || 'Local user') + '</small><div class="markdown-body">' + renderMarkdown(entry.body || '') + '</div>' +
17
- (entry.verifiedCommit ? '<section><h3>Resolution</h3><p>' + esc(entry.resolutionSummary) + '</p><code>' + esc(entry.verifiedCommit.sha) + '</code><p>' + esc(entry.verifiedCommit.subject) + '</p></section>' : '') +
18
- (entry.closeReason ? '<section class="issue-evidence"><h3>Closure</h3><p>' + esc(entry.closeReason.replace(/_/g, ' ')) + '</p></section>' : '') +
19
- '<div class="issue-actions"><button data-issue-edit>Edit</button><button data-issue-work' + (entry.status === 'resolved' || entry.status === 'closed' ? ' disabled title="Reopen this issue to start work"' : '') + '>Start work</button><button data-issue-history>History</button><button data-issue-copy>Copy reference</button>' + (entry.canDelete ? '<button data-issue-delete>Delete issue</button>' : '') + '</div>' +
20
- ((entry.linkedWorkSessions || []).length ? '<section><h3>Linked work sessions</h3>' + entry.linkedWorkSessions.map(function (link) { return '<button data-issue-session="' + esc(link.sessionId) + '">Open Driver session</button>'; }).join('') + '</section>' : '') +
21
- ((entry.resolutionHistory || []).length ? '<section class="issue-evidence"><h3>Resolution history</h3>' + entry.resolutionHistory.map(function (item) { return '<p>' + esc(item.resolutionSummary) + ' <code>' + esc(item.verifiedCommit && item.verifiedCommit.sha) + '</code></p>'; }).join('') + '</section>' : '') + '<section class="issue-discussion"><h3>Discussion' + (comments ? ' (' + entry.comments.length + ')' : '') + '</h3>' + (comments || '<p class="issue-discussion-empty">No comments yet.</p>') + '<form id="issue-comment-form"><label for="issue-comment-input">Add a comment</label><textarea id="issue-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea><p class="issue-comment-status" role="status"></p><button type="submit">Comment</button></form></section></div>';
32
+ var comments = (entry.comments || []).map(renderComment).join('');
33
+ var closed = entry.status === 'resolved' || entry.status === 'closed';
34
+ var time = entry.updatedAt || entry.createdAt;
35
+ var byline = [authorLine(entry), time ? new Date(time).toLocaleString() : '', 'v' + entry.revision].filter(Boolean).join(' · ');
36
+ return '<article class="issue-detail"><div class="issue-doc-chips"><span class="issue-type">' + esc(entry.type) + '</span>' + priorityChip(entry.priority) + statusChip(entry.status) + '</div>' +
37
+ '<h2 class="issue-doc-title">' + esc(entry.title) + '</h2>' + prose(entry.summary, 'issue-doc-summary') + '<div class="issue-byline">' + esc(byline) + '</div>' +
38
+ '<div class="issue-actions">' + action('data-issue-work', 'play', 'Start work', ' class="issue-primary"' + (closed ? ' disabled title="Reopen this issue to start work"' : '')) +
39
+ action('data-issue-edit', 'pencil', 'Edit') + action('data-issue-history', 'history', 'History') + action('data-issue-copy', 'link', 'Copy reference') +
40
+ (entry.canDelete ? action('data-issue-delete', 'trash-2', 'Delete', ' class="issue-danger"') : '') + '</div>' +
41
+ prose(entry.body, 'issue-doc-body') +
42
+ (entry.verifiedCommit ? '<section class="issue-evidence"><h3>Resolution</h3>' + prose(entry.resolutionSummary) + '<code>' + esc(entry.verifiedCommit.sha) + '</code><p>' + esc(entry.verifiedCommit.subject) + '</p></section>' : '') +
43
+ (entry.closeReason ? '<section class="issue-evidence"><h3>Closure</h3><p>' + esc(label(entry.closeReason)) + '</p></section>' : '') +
44
+ ((entry.linkedWorkSessions || []).length ? '<section class="issue-evidence"><h3>Linked work sessions</h3>' + entry.linkedWorkSessions.map(function (link) { return action('data-issue-session="' + esc(link.sessionId) + '"', 'messages-square', 'Open Driver session'); }).join('') + '</section>' : '') +
45
+ ((entry.resolutionHistory || []).length ? '<section class="issue-evidence"><h3>Resolution history</h3>' + entry.resolutionHistory.map(function (item) { return prose(item.resolutionSummary) + '<code>' + esc(item.verifiedCommit && item.verifiedCommit.sha) + '</code>'; }).join('') + '</section>' : '') +
46
+ '<section class="issue-discussion"><h3>Discussion' + (comments ? ' <span>' + entry.comments.length + '</span>' : '') + '</h3>' + (comments || '<p class="issue-discussion-empty">No comments yet.</p>') +
47
+ '<form id="issue-comment-form"><label for="issue-comment-input">Add a comment</label><textarea id="issue-comment-input" rows="3" required placeholder="Add context, a correction, or a question."></textarea><p class="issue-comment-status" role="status"></p><div class="issue-comment-actions"><button type="submit" class="issue-primary">Comment</button></div></form></section></article>';
22
48
  }
23
49
  export function renderIssueForm(entry) {
24
50
  var item = entry || {};
@@ -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
+ }