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.
- package/lib/context-usage.js +47 -0
- package/lib/issues-mcp-server.js +1 -1
- package/lib/issues-service.js +3 -1
- package/lib/loop-registry-occurrence.js +1 -0
- package/lib/notes.js +20 -3
- package/lib/os-users.js +6 -0
- package/lib/project-connection.js +14 -3
- package/lib/project-file-path.js +13 -0
- package/lib/project-file-watch.js +19 -8
- package/lib/project-filesystem.js +10 -16
- package/lib/project-http.js +53 -48
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project-logs-schema.js +1 -1
- package/lib/project-logs-service.js +46 -13
- package/lib/project-logs-store.js +17 -15
- package/lib/project-logs.js +70 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-scheduled-result-store.js +131 -0
- package/lib/project-scheduled-task-execution.js +35 -2
- package/lib/project-session-notes.js +11 -4
- package/lib/project-sessions.js +7 -0
- package/lib/project-user-message.js +47 -0
- package/lib/project.js +53 -3
- package/lib/public/app.js +26 -9
- package/lib/public/css/avatar-imprints.css +1 -1
- package/lib/public/css/command-palette.css +20 -0
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- package/lib/public/css/issues.css +78 -31
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/notifications-center.css +3 -3
- package/lib/public/css/project-logs.css +15 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +29 -0
- package/lib/public/modules/app-connection.js +9 -1
- package/lib/public/modules/app-favicon.js +10 -0
- package/lib/public/modules/app-messages.js +43 -12
- package/lib/public/modules/app-notifications.js +2 -0
- package/lib/public/modules/app-panels.js +77 -23
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rate-limit.js +148 -61
- package/lib/public/modules/avatar-imprint.js +25 -70
- package/lib/public/modules/avatar.js +12 -4
- package/lib/public/modules/chat-render-runtime.js +20 -0
- package/lib/public/modules/clay-issue-links.js +23 -2
- package/lib/public/modules/clay-log-links.js +11 -1
- package/lib/public/modules/context-view-preference.js +143 -0
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/filebrowser-tabs.js +1 -0
- package/lib/public/modules/filebrowser.js +45 -18
- package/lib/public/modules/issues-render.js +36 -10
- package/lib/public/modules/issues.js +95 -13
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/pane-bridge.js +14 -8
- package/lib/public/modules/pane-issue-bridge.js +27 -0
- package/lib/public/modules/permission-hints.js +13 -0
- package/lib/public/modules/profile.js +9 -9
- package/lib/public/modules/project-activation.js +49 -0
- package/lib/public/modules/project-logs-render.js +13 -3
- package/lib/public/modules/project-logs.js +45 -15
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-result-client.js +33 -0
- package/lib/public/modules/scheduled-result-display.js +50 -0
- package/lib/public/modules/scheduled-result-notification.js +31 -0
- package/lib/public/modules/scheduled-task-presentation.js +15 -0
- package/lib/public/modules/scheduled-tasks.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/split-session-boundary.js +28 -0
- package/lib/public/modules/split-view.js +42 -7
- package/lib/public/modules/sticky-notes-browser.js +130 -18
- package/lib/public/modules/sticky-notes.js +2 -3
- package/lib/public/modules/terminal.js +4 -4
- package/lib/public/modules/thinking-lifecycle.js +4 -1
- package/lib/public/modules/tools.js +5 -1
- package/lib/scheduled-run-state.js +10 -2
- package/lib/scheduler.js +1 -0
- package/lib/sdk-bridge.js +97 -61
- package/lib/sdk-message-processor.js +44 -13
- package/lib/sdk-skill-discovery.js +6 -28
- package/lib/sdk-turn-queue.js +14 -0
- package/lib/server-context-view.js +53 -0
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +23 -6
- package/lib/session-notes-mcp-server.js +3 -5
- package/lib/session-pair-factory.js +2 -2
- package/lib/session-title-generator.js +89 -0
- package/lib/session-title-policy.js +8 -2
- package/lib/sessions.js +26 -1
- package/lib/user-presence.js +6 -0
- package/lib/users-context-view-preferences.js +65 -0
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +12 -0
- package/lib/ws-schema.js +11 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/acp.js +3 -2
- package/lib/yoke/adapters/antigravity.js +2 -2
- package/lib/yoke/adapters/claude-worker.js +11 -12
- package/lib/yoke/adapters/claude.js +75 -22
- package/lib/yoke/adapters/codex.js +216 -29
- package/lib/yoke/adapters/kiro.js +6 -5
- package/lib/yoke/claude-context-usage.js +57 -0
- package/lib/yoke/claude-message-correlation.js +31 -0
- package/lib/yoke/claude-model-switch.js +15 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/mcp-bridge-server.js +18 -5
- package/lib/yoke/session-skill-catalog.js +45 -0
- package/lib/yoke/skill-discovery.js +60 -20
- package/lib/yoke/vendor-registry.js +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'<
|
|
20
|
-
|
|
21
|
-
(
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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>
|
|
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)
|
|
208
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}
|