clay-server 4.1.0-beta.8 → 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-pending-message-queue.js +71 -7
- package/lib/project-scheduled-messages.js +235 -52
- 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 +72 -5
- package/lib/project.js +60 -5
- package/lib/public/app.js +27 -12
- 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/input.css +0 -109
- 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/pending-message-queue.css +3 -0
- 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 +49 -37
- 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 +3 -0
- package/lib/public/modules/app-rate-limit.js +148 -243
- 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 -2
- 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/input.js +81 -20
- 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/pending-message-queue.js +43 -1
- 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-message-state.js +12 -36
- 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-pair-mcp-server.js +5 -4
- 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
|
@@ -11,6 +11,7 @@ import { showToast } from './utils.js';
|
|
|
11
11
|
import { isShellCommandMode, submitShellCommand } from './shell-command.js';
|
|
12
12
|
import { showPasteModal } from './paste-modal.js';
|
|
13
13
|
import { prepareAutonomousPayload } from './autonomous-run.js';
|
|
14
|
+
export { setScheduleBtnDisabled } from './scheduled-message-state.js';
|
|
14
15
|
|
|
15
16
|
var ctx;
|
|
16
17
|
|
|
@@ -25,6 +26,7 @@ var isComposing = false;
|
|
|
25
26
|
var isRemoteInput = false;
|
|
26
27
|
var scheduleDelayMs = 0; // 0 = no schedule, >0 = delay in ms
|
|
27
28
|
var _setScheduleDelayFn = null; // set by initInput
|
|
29
|
+
var scheduleRequestCounter = 0;
|
|
28
30
|
|
|
29
31
|
export function hasSendableContent() {
|
|
30
32
|
return !!(
|
|
@@ -58,19 +60,6 @@ export function clearScheduleDelay() {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
export function setScheduleBtnDisabled(disabled) {
|
|
62
|
-
var btn = document.getElementById("schedule-btn");
|
|
63
|
-
if (!btn) return;
|
|
64
|
-
btn.disabled = disabled;
|
|
65
|
-
if (disabled) {
|
|
66
|
-
btn.style.opacity = "0.3";
|
|
67
|
-
btn.style.pointerEvents = "none";
|
|
68
|
-
} else {
|
|
69
|
-
btn.style.opacity = "";
|
|
70
|
-
btn.style.pointerEvents = "";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
63
|
export var builtinCommands = [
|
|
75
64
|
{ name: "clear", desc: "Clear conversation" },
|
|
76
65
|
{ name: "context", desc: "Context window usage" },
|
|
@@ -94,6 +83,24 @@ function commitVendorForTurn() {
|
|
|
94
83
|
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
95
84
|
}
|
|
96
85
|
|
|
86
|
+
function scheduleAdmissionMap() {
|
|
87
|
+
return store.get("pendingScheduleAdmissions") || {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scheduleDraftMatches(left, right) {
|
|
91
|
+
return !!left && !!right && JSON.stringify(left) === JSON.stringify(right);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function scheduleContextMatches(left, right) {
|
|
95
|
+
return !!left && !!right && left.projectSlug === right.projectSlug && String(left.sessionId) === String(right.sessionId) && left.accountId === right.accountId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
store.subscribe(function (state, previous) {
|
|
99
|
+
if (state.currentSlug === previous.currentSlug && state.activeSessionId === previous.activeSessionId &&
|
|
100
|
+
state.myUserId === previous.myUserId && state.connected === previous.connected) return;
|
|
101
|
+
if (Object.keys(scheduleAdmissionMap()).length > 0) store.set({ pendingScheduleAdmissions: {}, pendingScheduleAdmission: null });
|
|
102
|
+
});
|
|
103
|
+
|
|
97
104
|
function showPreThinkingForTurn() {
|
|
98
105
|
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
99
106
|
ctx.showMatePreThinking();
|
|
@@ -295,13 +302,37 @@ export function sendMessage() {
|
|
|
295
302
|
// Scheduled message: queue message with timer delay
|
|
296
303
|
if (scheduleDelayMs > 0) {
|
|
297
304
|
var resetsAt = Date.now() + scheduleDelayMs;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
var scheduleContext = {
|
|
306
|
+
projectSlug: store.get("currentSlug"),
|
|
307
|
+
sessionId: store.get("activeSessionId"),
|
|
308
|
+
accountId: store.get("myUserId") || "default",
|
|
309
|
+
};
|
|
310
|
+
var scheduleDraft = { text: ctx.inputEl.value, images: images, pastes: pastes, files: files, delay: scheduleDelayMs };
|
|
311
|
+
var admissions = scheduleAdmissionMap();
|
|
312
|
+
var scheduleRequestId = null;
|
|
313
|
+
var admissionIds = Object.keys(admissions);
|
|
314
|
+
for (var admissionIndex = 0; admissionIndex < admissionIds.length; admissionIndex++) {
|
|
315
|
+
var candidate = admissions[admissionIds[admissionIndex]];
|
|
316
|
+
if (candidate.context.projectSlug === scheduleContext.projectSlug && String(candidate.context.sessionId) === String(scheduleContext.sessionId) &&
|
|
317
|
+
candidate.context.accountId === scheduleContext.accountId && scheduleDraftMatches(candidate.draft, scheduleDraft)) {
|
|
318
|
+
scheduleRequestId = candidate.requestId;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!scheduleRequestId) {
|
|
323
|
+
scheduleRequestCounter++;
|
|
324
|
+
scheduleRequestId = "schedule-" + Date.now().toString(36) + "-" + scheduleRequestCounter;
|
|
325
|
+
admissions = Object.assign({}, admissions);
|
|
326
|
+
admissions[scheduleRequestId] = { requestId: scheduleRequestId, context: scheduleContext, draft: scheduleDraft };
|
|
327
|
+
var boundedIds = Object.keys(admissions);
|
|
328
|
+
while (boundedIds.length > 8) delete admissions[boundedIds.shift()];
|
|
329
|
+
}
|
|
330
|
+
store.set({ pendingScheduleAdmissions: admissions, pendingScheduleAdmission: admissions[scheduleRequestId] });
|
|
331
|
+
ctx.ws.send(JSON.stringify({
|
|
332
|
+
type: "schedule_message", requestId: scheduleRequestId,
|
|
333
|
+
projectSlug: scheduleContext.projectSlug, sessionId: scheduleContext.sessionId, accountId: scheduleContext.accountId,
|
|
334
|
+
text: text || "", images: images, pastes: pastes, resetsAt: resetsAt,
|
|
335
|
+
}));
|
|
305
336
|
return;
|
|
306
337
|
}
|
|
307
338
|
|
|
@@ -348,6 +379,36 @@ export function handleAcceptedOrdinaryMessage() {
|
|
|
348
379
|
showPreThinkingForTurn();
|
|
349
380
|
}
|
|
350
381
|
|
|
382
|
+
export function handleScheduleMessageResult(msg) {
|
|
383
|
+
var admissions = scheduleAdmissionMap();
|
|
384
|
+
var pending = admissions[msg && msg.requestId];
|
|
385
|
+
var currentContext = { projectSlug: store.get("currentSlug"), sessionId: store.get("activeSessionId"), accountId: store.get("myUserId") || "default" };
|
|
386
|
+
if (!pending || !msg || msg.requestId !== pending.requestId) return false;
|
|
387
|
+
if (!store.get("connected") || !scheduleContextMatches(pending.context, currentContext) ||
|
|
388
|
+
msg.projectSlug !== currentContext.projectSlug || String(msg.sessionId) !== String(currentContext.sessionId) || msg.accountId !== currentContext.accountId) {
|
|
389
|
+
admissions = Object.assign({}, admissions);
|
|
390
|
+
delete admissions[msg.requestId];
|
|
391
|
+
store.set({ pendingScheduleAdmissions: admissions, pendingScheduleAdmission: null });
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (!msg.ok) {
|
|
395
|
+
showToast(msg.error || "Could not schedule the message.", "error");
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
admissions = Object.assign({}, admissions);
|
|
399
|
+
delete admissions[msg.requestId];
|
|
400
|
+
store.set({ pendingScheduleAdmissions: admissions, pendingScheduleAdmission: null });
|
|
401
|
+
var currentDraft = { text: ctx.inputEl.value, images: pendingImages, pastes: pendingPastes.map(function (p) { return p.text; }), files: pendingFiles, delay: scheduleDelayMs };
|
|
402
|
+
if (JSON.stringify(currentDraft) !== JSON.stringify(pending.draft)) return true;
|
|
403
|
+
clearScheduleDelay();
|
|
404
|
+
ctx.inputEl.value = "";
|
|
405
|
+
sendInputSync();
|
|
406
|
+
clearPendingImages();
|
|
407
|
+
autoResize();
|
|
408
|
+
ctx.inputEl.focus();
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
351
412
|
export function sendTextMessage(text) {
|
|
352
413
|
if (!ctx || !ctx.inputEl || typeof text !== "string" || !text.trim()) return false;
|
|
353
414
|
clearPendingImages();
|
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,7 @@ import { escapeHtml } from './utils.js';
|
|
|
4
4
|
import { refreshIcons } from './icons.js';
|
|
5
5
|
import { activeQueueItems, moveQueueId, matchesQueueContext, canApplyQueueRevision } from './pending-message-queue-model.js';
|
|
6
6
|
import { imageSrc, imageDraft, openImagePreview, chooseImages } from './pending-message-image-editor.js';
|
|
7
|
+
import { setScheduleBtnDisabled } from './scheduled-message-state.js';
|
|
7
8
|
|
|
8
9
|
var queueEl = null;
|
|
9
10
|
var initialized = false;
|
|
@@ -34,6 +35,21 @@ function isOwn(item) {
|
|
|
34
35
|
return !!item && String(item.actorId) === String(store.get('myUserId'));
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export function hasFutureScheduledPending(items) {
|
|
39
|
+
items = items || [];
|
|
40
|
+
for (var i = 0; i < items.length; i++) {
|
|
41
|
+
if (items[i].state === "pending" && items[i].schedule && items[i].schedule.ready !== true) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function syncScheduleButton() {
|
|
47
|
+
var target = context();
|
|
48
|
+
var queue = currentQueue();
|
|
49
|
+
var current = target.enabled && queue.projectSlug === target.projectSlug && sameId(queue.sessionId, target.sessionId);
|
|
50
|
+
setScheduleBtnDisabled(current && hasFutureScheduledPending(queue.items));
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
function isEditableItem(id) {
|
|
38
54
|
var items = currentQueue().items || [];
|
|
39
55
|
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].state === "pending" && isOwn(items[i]);
|
|
@@ -116,6 +132,25 @@ function sendMutation(kind, fields) {
|
|
|
116
132
|
return true;
|
|
117
133
|
}
|
|
118
134
|
|
|
135
|
+
function sendScheduledNow(id) {
|
|
136
|
+
var target = context();
|
|
137
|
+
var queue = currentQueue();
|
|
138
|
+
var item = (queue.items || []).find(function (candidate) { return candidate.id === id; });
|
|
139
|
+
if (!target.enabled || queue.projectSlug !== target.projectSlug || !sameId(queue.sessionId, target.sessionId) || !item || item.state !== "pending" || !isOwn(item) || !item.schedule || item.schedule.ready === true) return false;
|
|
140
|
+
var requestId = rememberRequest("send-now", { itemId: id, jobId: item.schedule.jobId, jobRevision: item.schedule.revision, queueRevision: queue.revision });
|
|
141
|
+
if (!send({ type: "send_scheduled_now", projectSlug: target.projectSlug, sessionId: target.sessionId, jobId: item.schedule.jobId, revision: item.schedule.revision, queueRevision: queue.revision, requestId: requestId })) {
|
|
142
|
+
forgetRequest(requestId);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function scheduledTimeLabel(notBefore) {
|
|
149
|
+
var date = new Date(Number(notBefore));
|
|
150
|
+
if (!isFinite(date.getTime())) return "Scheduled";
|
|
151
|
+
return "Scheduled for " + date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
function applyCanonical(result) {
|
|
120
155
|
var target = context();
|
|
121
156
|
if (!result || result.projectSlug !== target.projectSlug || !sameId(result.sessionId, target.sessionId)) return false;
|
|
@@ -302,8 +337,10 @@ function renderItem(item, index) {
|
|
|
302
337
|
var isEditing = editing && editing.id === item.id && item.state === "pending" && own;
|
|
303
338
|
var attribution = own ? "You" : (item.actorName || "Collaborator");
|
|
304
339
|
var attachment = attachmentLabel(item);
|
|
340
|
+
var scheduled = item.schedule && item.schedule.jobId;
|
|
341
|
+
var futureScheduled = scheduled && item.schedule.ready !== true;
|
|
305
342
|
var text = (item.message && item.message.text || "").trim() || "Attachment-only message";
|
|
306
|
-
var html = '<div class="pending-message-chip' + (item.state === "claimed" ? ' is-claimed' : '') + '" data-pending-id="' + escapeHtml(item.id) + '">';
|
|
343
|
+
var html = '<div class="pending-message-chip' + (item.state === "claimed" ? ' is-claimed' : '') + (scheduled ? ' is-scheduled' : '') + (futureScheduled ? ' is-future' : '') + '" data-pending-id="' + escapeHtml(item.id) + '">';
|
|
307
344
|
html += '<span class="pending-message-number" aria-hidden="true">' + (index + 1) + '</span>';
|
|
308
345
|
if (own && item.state === "pending") html += '<button class="pending-message-drag" type="button" data-action="drag" aria-label="Reorder queued message ' + (index + 1) + '" title="Drag, or use arrow keys to reorder">' + '<i data-lucide="grip-vertical"></i></button>';
|
|
309
346
|
else html += '<span class="pending-message-drag is-static"><i data-lucide="' + (item.state === "claimed" ? "loader-circle" : "lock-keyhole") + '"></i></span>';
|
|
@@ -331,15 +368,18 @@ function renderItem(item, index) {
|
|
|
331
368
|
if (attachment) html += '<span>' + escapeHtml(attachment) + '</span>';
|
|
332
369
|
if (item.state === "claimed") html += '<span class="pending-message-sending">Sending</span>';
|
|
333
370
|
html += '</div>';
|
|
371
|
+
if (scheduled) html += '<div class="pending-message-schedule-meta"><i data-lucide="clock-3"></i><span>' + escapeHtml(futureScheduled ? scheduledTimeLabel(item.schedule.notBefore) : "Ready to send") + '</span></div>';
|
|
334
372
|
}
|
|
335
373
|
html += '</div><div class="pending-message-actions">';
|
|
336
374
|
if (!isEditing && own && item.state === "pending") html += '<button type="button" data-action="edit" aria-label="Edit queued message" title="Edit"><i data-lucide="pencil"></i></button>';
|
|
375
|
+
if (!isEditing && own && item.state === "pending" && futureScheduled) html += '<button type="button" data-action="send-now" aria-label="Send scheduled message now" title="Send now"><i data-lucide="send"></i></button>';
|
|
337
376
|
if (!isEditing && own) html += '<button type="button" data-action="cancel" aria-label="Cancel queued message" title="Cancel"><i data-lucide="x"></i></button>';
|
|
338
377
|
html += '</div></div>';
|
|
339
378
|
return html;
|
|
340
379
|
}
|
|
341
380
|
|
|
342
381
|
function render() {
|
|
382
|
+
syncScheduleButton();
|
|
343
383
|
if (!queueEl) queueEl = document.getElementById("pending-message-queue");
|
|
344
384
|
if (!queueEl) return;
|
|
345
385
|
var target = context();
|
|
@@ -382,6 +422,7 @@ function bindEvents() {
|
|
|
382
422
|
} else if (action === "edit") beginEdit(id);
|
|
383
423
|
else if (action === "edit-cancel") store.set({ pendingMessageQueueFocusReturn: { id: id, action: "edit" }, pendingMessageQueueEdit: null });
|
|
384
424
|
else if (action === "edit-save") saveEdit(id);
|
|
425
|
+
else if (action === "send-now") sendScheduledNow(id);
|
|
385
426
|
else if (action === "cancel") sendMutation("cancel", { id: id });
|
|
386
427
|
else if (action === "resume") sendMutation("resume", {});
|
|
387
428
|
});
|
|
@@ -437,6 +478,7 @@ export function initPendingMessageQueue() {
|
|
|
437
478
|
var contextChanged = state.connected !== previous.connected || state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.activeSessionMode !== previous.activeSessionMode || state.dmMode !== previous.dmMode || state.mateProjectSlug !== previous.mateProjectSlug || state.myUserId !== previous.myUserId || state.isMultiUserMode !== previous.isMultiUserMode;
|
|
438
479
|
if (contextChanged) {
|
|
439
480
|
store.set({ pendingMessageQueue: emptyQueue(), pendingMessageQueueRequests: {}, pendingMessageQueueEdit: null, pendingMessageQueueDrag: null, pendingMessageQueueFocusReturn: null });
|
|
481
|
+
setScheduleBtnDisabled(false);
|
|
440
482
|
if (context().enabled) requestPendingMessages();
|
|
441
483
|
return;
|
|
442
484
|
}
|
|
@@ -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
|
+
}
|