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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// User profile module — profile popovers for people and Mates
|
|
2
2
|
// Stores profile server-side in ~/.clay/profile.json
|
|
3
|
-
// Generated identities use deterministic
|
|
3
|
+
// Generated identities use deterministic local pixel identicons.
|
|
4
4
|
|
|
5
5
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
6
6
|
import { setSTTLang } from './stt.js';
|
|
7
|
-
import { avatarUrl, mateAvatarUrl, mateMarkUrl } from './avatar.js';
|
|
7
|
+
import { avatarUrl, mateAvatarUrl, mateMarkUrl, userAvatarSeed } from './avatar.js';
|
|
8
8
|
import { store } from './store.js';
|
|
9
9
|
import { applyTerminalFont } from './terminal-prefs.js';
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ var popoverEl = null;
|
|
|
15
15
|
var saveTimer = null;
|
|
16
16
|
|
|
17
17
|
function getAvatarSeed() {
|
|
18
|
-
return profile.avatarSeed || 'anonymous';
|
|
18
|
+
return profile.avatarSeed || String(profileUsername || 'anonymous').substring(0, 30);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// --- API ---
|
|
@@ -307,14 +307,14 @@ function showPopover() {
|
|
|
307
307
|
html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
|
|
308
308
|
html += '</div>';
|
|
309
309
|
|
|
310
|
-
//
|
|
310
|
+
// Pixel identicon or custom photo
|
|
311
311
|
html += '<div class="profile-field">';
|
|
312
312
|
html += '<label class="profile-field-label">Identity</label>';
|
|
313
313
|
html += '<div class="profile-imprint-panel">';
|
|
314
|
-
html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated
|
|
314
|
+
html += '<div class="profile-imprint-preview"><img src="' + avatarUrl(currentStyle, seed, 72) + '" alt="Generated pixel avatar"><span><strong>Pixel avatar</strong><small>Unique to this identity</small></span></div>';
|
|
315
315
|
html += '<div class="profile-imprint-actions">';
|
|
316
|
-
html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use
|
|
317
|
-
html += '<button class="profile-regenerate-imprint" type="button" title="Generate another
|
|
316
|
+
html += '<button class="profile-use-imprint' + (!profile.avatarCustom ? ' is-active' : '') + '" type="button">Use pixel avatar</button>';
|
|
317
|
+
html += '<button class="profile-regenerate-imprint" type="button" title="Generate another pixel avatar">' + iconHtml('shuffle') + ' New avatar</button>';
|
|
318
318
|
html += '<button class="profile-avatar-upload' + (profile.avatarCustom ? ' is-active' : '') + '" type="button">Upload photo</button>';
|
|
319
319
|
html += '</div>';
|
|
320
320
|
html += '</div>';
|
|
@@ -511,9 +511,9 @@ export function initProfile(_ctx) {
|
|
|
511
511
|
if (data.avatarCustom) profile.avatarCustom = data.avatarCustom;
|
|
512
512
|
if (data.username) profileUsername = data.username;
|
|
513
513
|
|
|
514
|
-
//
|
|
514
|
+
// Persist a stable account identity only when the server has not saved a seed.
|
|
515
515
|
if (!profile.avatarSeed) {
|
|
516
|
-
profile.avatarSeed =
|
|
516
|
+
profile.avatarSeed = userAvatarSeed(data);
|
|
517
517
|
saveProfile();
|
|
518
518
|
}
|
|
519
519
|
|
|
@@ -43,3 +43,52 @@ export function isProjectContextConnected(state, slug, socket) {
|
|
|
43
43
|
&& state.connected === true
|
|
44
44
|
&& state.activeProjectSlug === slug;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export function isProjectSessionReady(state, socket) {
|
|
48
|
+
var target = projectWsPath(state.currentSlug);
|
|
49
|
+
return !!socket
|
|
50
|
+
&& socket.readyState === 1
|
|
51
|
+
&& state.connected === true
|
|
52
|
+
&& !!state.currentSlug
|
|
53
|
+
&& state.wsPath === target
|
|
54
|
+
&& state.socketPath === target
|
|
55
|
+
&& state.activeProjectSlug === state.currentSlug
|
|
56
|
+
&& state.sessionActivatedProjectSlug === state.currentSlug
|
|
57
|
+
&& state.sessionListProjectSlug === state.currentSlug
|
|
58
|
+
&& state.splitGroupsProjectSlug === state.currentSlug;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function splitPanesMatchProject(split, slug) {
|
|
62
|
+
if (!split || !Array.isArray(split.panes) || split.panes.length !== 2 || !slug) return false;
|
|
63
|
+
return split.panes[0].slug === slug && split.panes[1].slug === slug;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function hasCurrentSplitGroup(split, groups) {
|
|
67
|
+
if (!split || !split.groupId) return true;
|
|
68
|
+
var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
|
|
69
|
+
if (!members || members.length !== 2) return false;
|
|
70
|
+
var available = Array.isArray(groups) ? groups : [];
|
|
71
|
+
for (var i = 0; i < available.length; i++) {
|
|
72
|
+
var group = available[i];
|
|
73
|
+
if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !== 2) continue;
|
|
74
|
+
if (group.members.indexOf(members[0]) !== -1 && group.members.indexOf(members[1]) !== -1) return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function reconcileRestoredSplit(split, groups) {
|
|
80
|
+
if (!split || !split.groupId) return { action: "none" };
|
|
81
|
+
var available = Array.isArray(groups) ? groups : [];
|
|
82
|
+
for (var i = 0; i < available.length; i++) {
|
|
83
|
+
var group = available[i];
|
|
84
|
+
if (group.id !== split.groupId) continue;
|
|
85
|
+
var oldMembers = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
|
|
86
|
+
var members = Array.isArray(group.members) ? group.members : [];
|
|
87
|
+
if (oldMembers && oldMembers.length === 2 && members.length === 2
|
|
88
|
+
&& members.indexOf(oldMembers[0]) !== -1 && members.indexOf(oldMembers[1]) !== -1) {
|
|
89
|
+
return { action: "keep", group: group };
|
|
90
|
+
}
|
|
91
|
+
return { action: "rebuild", group: group };
|
|
92
|
+
}
|
|
93
|
+
return { action: "clear" };
|
|
94
|
+
}
|
|
@@ -43,8 +43,9 @@ function actorLabel(actor) {
|
|
|
43
43
|
// object (userId, sessionKey, vendor), but a session-authored entry must not
|
|
44
44
|
// read as if the human owner wrote it: the Project Driver did. The owner is
|
|
45
45
|
// only the account the session ran under.
|
|
46
|
-
function authorLine(entry) {
|
|
46
|
+
export function authorLine(entry) {
|
|
47
47
|
var actor = (entry && (entry.updatedBy || entry.createdBy)) || {};
|
|
48
|
+
if (actor.type === "system") return actor.displayName || "Scheduled Tasks";
|
|
48
49
|
if (actor.type === "session") {
|
|
49
50
|
return actor.vendor ? "Project Driver (" + vendorLabel(actor.vendor) + ")" : "Project Driver";
|
|
50
51
|
}
|
|
@@ -62,7 +63,7 @@ function formatTime(value) {
|
|
|
62
63
|
catch (e) { return ""; }
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function relativeTime(value) {
|
|
66
|
+
export function relativeTime(value) {
|
|
66
67
|
if (!value) return "";
|
|
67
68
|
var delta = Date.now() - value;
|
|
68
69
|
if (delta < 0) return formatTime(value);
|
|
@@ -363,7 +364,7 @@ export function renderDetail(detailEl, entry, handlers) {
|
|
|
363
364
|
formatTime(entry.updatedAt || entry.createdAt),
|
|
364
365
|
].filter(Boolean).join(" · ");
|
|
365
366
|
setText(header, ".project-log-byline", byline);
|
|
366
|
-
if (handlers && handlers.canDelete) {
|
|
367
|
+
if (handlers && handlers.canDelete && !entry.scheduledResult) {
|
|
367
368
|
var deleteButton = document.createElement("button");
|
|
368
369
|
deleteButton.type = "button";
|
|
369
370
|
deleteButton.className = "project-log-delete-button";
|
|
@@ -373,6 +374,15 @@ export function renderDetail(detailEl, entry, handlers) {
|
|
|
373
374
|
}
|
|
374
375
|
article.appendChild(header);
|
|
375
376
|
|
|
377
|
+
if (entry.scheduledResult && entry.scheduledResult.driverOriginId && handlers && handlers.onOpenSession) {
|
|
378
|
+
var sessionButton = document.createElement("button");
|
|
379
|
+
sessionButton.type = "button";
|
|
380
|
+
sessionButton.className = "project-log-session-button";
|
|
381
|
+
sessionButton.innerHTML = '<i data-lucide="external-link"></i><span>View session</span>';
|
|
382
|
+
sessionButton.addEventListener("click", function () { handlers.onOpenSession(entry); });
|
|
383
|
+
article.appendChild(sessionButton);
|
|
384
|
+
}
|
|
385
|
+
|
|
376
386
|
var markdown = document.createElement("div");
|
|
377
387
|
markdown.className = "project-log-markdown md-content";
|
|
378
388
|
markdown.innerHTML = renderMarkdown(entry.body || "");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Project Logs — the project's ledger of agent-authored decisions and work.
|
|
2
|
-
//
|
|
3
2
|
// A bounded right-side workbench pane, like the document viewer and terminal.
|
|
4
3
|
// Inside it there is one navigation stack rather than a master/detail split:
|
|
5
4
|
// the list fills the pane, opening an entry replaces it with the full-pane
|
|
@@ -14,10 +13,10 @@ import { store } from './store.js';
|
|
|
14
13
|
import { getWs } from './ws-ref.js';
|
|
15
14
|
import { refreshIcons } from './icons.js';
|
|
16
15
|
import { showToast } from './utils.js';
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
16
|
+
import { showScheduledResultNotification } from './scheduled-result-notification.js';
|
|
17
|
+
import * as scheduledResultClient from './scheduled-result-client.js';
|
|
18
|
+
import { clearPendingScheduledResultRead, handleScheduledResultDisplayed, initScheduledResultDisplay, openScheduledResultSession } from './scheduled-result-display.js';
|
|
19
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
21
20
|
import { renderFilter, renderContextFilter, renderList, renderDetail } from './project-logs-render.js';
|
|
22
21
|
|
|
23
22
|
var panel = null;
|
|
@@ -35,7 +34,7 @@ var requestCounter = 0;
|
|
|
35
34
|
function syncUnreadBadge() {
|
|
36
35
|
var button = document.getElementById("project-logs-btn");
|
|
37
36
|
if (!button) return;
|
|
38
|
-
var unread = store.get('projectLogsUnread') || 0;
|
|
37
|
+
var unread = (store.get('projectLogsUnread') || 0) + (store.get('scheduledResultUnread') || 0);
|
|
39
38
|
button.classList.toggle("project-logs-unread", unread > 0);
|
|
40
39
|
var badge = document.getElementById("project-logs-count");
|
|
41
40
|
if (!badge) return;
|
|
@@ -164,10 +163,10 @@ function applyWindowState(wide, fullscreen) {
|
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
// --- Navigation stack ----------------------------------------------------
|
|
167
|
-
|
|
168
166
|
function showList() {
|
|
169
167
|
if (!panel) return;
|
|
170
168
|
store.set({ projectLogsView: "list", projectLogsSelectedRef: null });
|
|
169
|
+
clearPendingScheduledResultRead();
|
|
171
170
|
detailEl.classList.add("hidden");
|
|
172
171
|
listEl.classList.remove("hidden");
|
|
173
172
|
panel.querySelector("#project-logs-toolbar").classList.remove("hidden");
|
|
@@ -188,8 +187,10 @@ function showDetail(entry) {
|
|
|
188
187
|
renderDetail(detailEl, entry, {
|
|
189
188
|
onComment: submitComment,
|
|
190
189
|
onDelete: openDeleteDialog,
|
|
190
|
+
onOpenSession: openScheduledResultSession,
|
|
191
191
|
canDelete: store.get('projectLogsCanDelete') === true,
|
|
192
192
|
});
|
|
193
|
+
handleScheduledResultDisplayed(entry);
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
function openDeleteDialog(entry) {
|
|
@@ -230,7 +231,6 @@ function confirmDelete() {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
// --- Requests ------------------------------------------------------------
|
|
233
|
-
|
|
234
234
|
function requestList() {
|
|
235
235
|
var requestId = nextRequestId("list-logs");
|
|
236
236
|
store.set({ projectLogsListRequestId: requestId });
|
|
@@ -282,14 +282,17 @@ function applyContextFilter(contextMode) {
|
|
|
282
282
|
// --- Lifecycle -----------------------------------------------------------
|
|
283
283
|
|
|
284
284
|
export function initProjectLogs() {
|
|
285
|
+
registerRightWorkbench("project-logs", closeProjectLogs);
|
|
285
286
|
var button = document.getElementById("project-logs-btn");
|
|
286
287
|
if (!button) return;
|
|
287
288
|
button.addEventListener("click", function () {
|
|
288
289
|
if (store.get('projectLogsOpen')) closeProjectLogs();
|
|
289
290
|
else openProjectLogs();
|
|
290
291
|
});
|
|
292
|
+
initScheduledResultDisplay();
|
|
291
293
|
syncUnreadBadge();
|
|
292
294
|
store.subscribe(function (state, previous) {
|
|
295
|
+
if (state.connected === false && previous.connected === true) store.set({ scheduledResultAutoFocusHandled: false });
|
|
293
296
|
if (state.currentSlug === previous.currentSlug) return;
|
|
294
297
|
closeProjectLogs();
|
|
295
298
|
applyWindowState(false, false);
|
|
@@ -309,7 +312,13 @@ export function initProjectLogs() {
|
|
|
309
312
|
projectLogsDeleteRequestId: null,
|
|
310
313
|
projectLogsCanDelete: false,
|
|
311
314
|
projectLogsUnread: 0,
|
|
312
|
-
projectLogsSeenRevisions: {}
|
|
315
|
+
projectLogsSeenRevisions: {},
|
|
316
|
+
scheduledResultUnread: 0,
|
|
317
|
+
scheduledResultAutoFocusHandled: false,
|
|
318
|
+
scheduledResultFocusRef: null,
|
|
319
|
+
scheduledResultPendingReadRef: null,
|
|
320
|
+
scheduledResultSeenRefs: {},
|
|
321
|
+
scheduledResultSessionRequestId: null,
|
|
313
322
|
});
|
|
314
323
|
syncUnreadBadge();
|
|
315
324
|
});
|
|
@@ -320,11 +329,7 @@ export function initProjectLogs() {
|
|
|
320
329
|
export function openProjectLogs() {
|
|
321
330
|
ensurePanel();
|
|
322
331
|
if (!panel) return;
|
|
323
|
-
|
|
324
|
-
closeNotesBrowser();
|
|
325
|
-
// Claim the single right workbench slot.
|
|
326
|
-
try { closeFileViewer(); } catch (e) {}
|
|
327
|
-
try { closeTerminal(); } catch (e) {}
|
|
332
|
+
claimRightWorkbench("project-logs");
|
|
328
333
|
panel.classList.remove("hidden");
|
|
329
334
|
applyWindowState(store.get('projectLogsWide'), false);
|
|
330
335
|
var button = document.getElementById("project-logs-btn");
|
|
@@ -344,6 +349,7 @@ export function openProjectLog(ref) {
|
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
export function closeProjectLogs() {
|
|
352
|
+
releaseRightWorkbench("project-logs");
|
|
347
353
|
if (!store.get('projectLogsOpen')) return;
|
|
348
354
|
if (panel) {
|
|
349
355
|
panel.classList.add("hidden");
|
|
@@ -384,11 +390,35 @@ export function handleProjectLogsState(msg) {
|
|
|
384
390
|
|
|
385
391
|
export function handleProjectLogEntry(msg) {
|
|
386
392
|
if (msg.requestId && msg.requestId !== store.get('projectLogsReadRequestId')) return;
|
|
387
|
-
if (!msg.entry) return;
|
|
393
|
+
if (!msg.entry || !store.get('projectLogsOpen')) return;
|
|
388
394
|
store.set({ projectLogsCanDelete: msg.canDelete === true });
|
|
389
395
|
showDetail(msg.entry);
|
|
390
396
|
}
|
|
391
397
|
|
|
398
|
+
export function handleScheduledTaskResultsState(msg) {
|
|
399
|
+
var transition = scheduledResultClient.scheduledResultStateTransition(store.snap(), msg);
|
|
400
|
+
store.set(transition.patch); syncUnreadBadge();
|
|
401
|
+
if (!transition.openRef) return false;
|
|
402
|
+
var opened = openProjectLog(transition.openRef);
|
|
403
|
+
if (!opened) store.set({ scheduledResultFocusRef: null });
|
|
404
|
+
return opened;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function handleScheduledTaskResultCreated(msg) {
|
|
408
|
+
var patch = scheduledResultClient.scheduledResultCreatedTransition(store.snap(), msg);
|
|
409
|
+
if (!patch) return false;
|
|
410
|
+
store.set(patch); syncUnreadBadge();
|
|
411
|
+
showScheduledResultNotification(msg.title || "Scheduled task finished", msg.summary || "", function () { openProjectLog(msg.ref); });
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function handleScheduledTaskResultSession(msg) {
|
|
416
|
+
var sessionId = scheduledResultClient.scheduledResultSessionId(store.snap(), msg);
|
|
417
|
+
if (sessionId === null) return false;
|
|
418
|
+
store.set({ scheduledResultSessionRequestId: null });
|
|
419
|
+
return send({ type: "switch_session", id: sessionId });
|
|
420
|
+
}
|
|
421
|
+
|
|
392
422
|
export function handleProjectLogCommented(msg) {
|
|
393
423
|
if (msg.requestId && msg.requestId !== store.get('projectLogsCommentRequestId')) return;
|
|
394
424
|
if (!msg.entry) return;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Single arbitration point for the mutually exclusive right-side workbench.
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
|
|
5
|
+
var closers = {};
|
|
6
|
+
|
|
7
|
+
export function registerRightWorkbench(name, close) {
|
|
8
|
+
if (!name || typeof close !== "function") return;
|
|
9
|
+
closers[name] = close;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function claimRightWorkbench(name) {
|
|
13
|
+
var previousRevision = store.get("rightWorkbenchRevision") || 0;
|
|
14
|
+
var nextRevision = previousRevision + 1;
|
|
15
|
+
storeSet(name, nextRevision);
|
|
16
|
+
var keys = Object.keys(closers);
|
|
17
|
+
for (var i = 0; i < keys.length; i++) {
|
|
18
|
+
if (keys[i] === name) continue;
|
|
19
|
+
try { closers[keys[i]](); } catch (error) { /* one panel must not block another */ }
|
|
20
|
+
}
|
|
21
|
+
return nextRevision;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function storeSet(name, value) {
|
|
25
|
+
store.set({ rightWorkbenchOwner: name, rightWorkbenchRevision: value });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function releaseRightWorkbench(name) {
|
|
29
|
+
if (store.get("rightWorkbenchOwner") !== name) return;
|
|
30
|
+
storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isRightWorkbenchCurrent(name, expectedRevision) {
|
|
34
|
+
return store.get("rightWorkbenchOwner") === name && store.get("rightWorkbenchRevision") === expectedRevision;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resetRightWorkbench() {
|
|
38
|
+
storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
store.subscribe(function (state, previous) {
|
|
42
|
+
if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return;
|
|
43
|
+
resetRightWorkbench();
|
|
44
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Pure Scheduled Tasks result-news decisions. The production controller owns
|
|
2
|
+
// store, WebSocket, DOM, and peer-module calls directly.
|
|
3
|
+
|
|
4
|
+
export function scheduledResultStateTransition(state, msg) {
|
|
5
|
+
var patch = { scheduledResultUnread: Number(msg.unreadCount) || 0 };
|
|
6
|
+
var openRef = null;
|
|
7
|
+
if (msg.autoFocus && msg.newestUnreadRef && !state.scheduledResultAutoFocusHandled) {
|
|
8
|
+
patch.scheduledResultAutoFocusHandled = true;
|
|
9
|
+
patch.scheduledResultFocusRef = msg.newestUnreadRef;
|
|
10
|
+
openRef = msg.newestUnreadRef;
|
|
11
|
+
}
|
|
12
|
+
return { patch: patch, openRef: openRef };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function scheduledResultCreatedTransition(state, msg) {
|
|
16
|
+
if (!msg || !msg.ref || (state.scheduledResultSeenRefs || {})[msg.ref]) return null;
|
|
17
|
+
var seen = Object.assign({}, state.scheduledResultSeenRefs || {});
|
|
18
|
+
seen[msg.ref] = true;
|
|
19
|
+
return { scheduledResultSeenRefs: seen, scheduledResultUnread: (state.scheduledResultUnread || 0) + 1 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldAcknowledgeScheduledResult(entry, state, visibilityState) {
|
|
23
|
+
return !!(entry && entry.scheduledResult && visibilityState !== "hidden" && state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === entry.ref);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shouldFocusScheduledResult(entry, state) {
|
|
27
|
+
return !!(entry && state.scheduledResultFocusRef === entry.ref);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function scheduledResultSessionId(state, msg) {
|
|
31
|
+
if (msg.requestId && msg.requestId !== state.scheduledResultSessionRequestId) return null;
|
|
32
|
+
return typeof msg.sessionId === "number" ? msg.sessionId : null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
import { shouldAcknowledgeScheduledResult, shouldFocusScheduledResult } from './scheduled-result-client.js';
|
|
5
|
+
|
|
6
|
+
function sendRead(ref) {
|
|
7
|
+
var ws = getWs();
|
|
8
|
+
if (!ws || ws.readyState !== 1) return false;
|
|
9
|
+
ws.send(JSON.stringify({ type: "scheduled_task_result_mark_read", ref: ref }));
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var openRequestCounter = 0;
|
|
14
|
+
export function openScheduledResultSession(entry) {
|
|
15
|
+
if (!entry || !entry.scheduledResult) return false;
|
|
16
|
+
openRequestCounter += 1;
|
|
17
|
+
var requestId = "open-scheduled-session-" + Date.now() + "-" + openRequestCounter;
|
|
18
|
+
store.set({ scheduledResultSessionRequestId: requestId });
|
|
19
|
+
var ws = getWs();
|
|
20
|
+
if (!ws || ws.readyState !== 1) { showToast("Reconnect to view this scheduled session.", "warn"); return false; }
|
|
21
|
+
ws.send(JSON.stringify({ type: "scheduled_task_result_open_session", requestId: requestId, ref: entry.ref })); return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function acknowledgeVisible() {
|
|
25
|
+
var ref = store.get('scheduledResultPendingReadRef');
|
|
26
|
+
if (!ref || document.visibilityState === "hidden") return;
|
|
27
|
+
store.set({ scheduledResultPendingReadRef: null });
|
|
28
|
+
var state = store.snap();
|
|
29
|
+
if (state.projectLogsOpen && state.projectLogsView === "detail" && state.projectLogsSelectedRef === ref) sendRead(ref);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearPendingScheduledResultRead() {
|
|
33
|
+
store.set({ scheduledResultPendingReadRef: null });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function handleScheduledResultDisplayed(entry) {
|
|
37
|
+
var state = store.snap();
|
|
38
|
+
if (shouldFocusScheduledResult(entry, state)) {
|
|
39
|
+
store.set({ scheduledResultFocusRef: null });
|
|
40
|
+
var heading = document.querySelector("#project-logs-detail .project-log-document h1");
|
|
41
|
+
if (heading) { heading.setAttribute("tabindex", "-1"); heading.focus(); }
|
|
42
|
+
}
|
|
43
|
+
if (shouldAcknowledgeScheduledResult(entry, store.snap(), document.visibilityState)) {
|
|
44
|
+
clearPendingScheduledResultRead(); sendRead(entry.ref);
|
|
45
|
+
} else if (entry && entry.scheduledResult && document.visibilityState === "hidden") store.set({ scheduledResultPendingReadRef: entry.ref });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function initScheduledResultDisplay() {
|
|
49
|
+
document.addEventListener("visibilitychange", acknowledgeVisible);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { refreshIcons, iconHtml } from './icons.js';
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
|
|
4
|
+
function removeBanner(banner) {
|
|
5
|
+
if (!banner || !banner.parentNode) return;
|
|
6
|
+
banner.classList.remove("show");
|
|
7
|
+
setTimeout(function () { if (banner.parentNode) banner.remove(); }, 220);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function showScheduledResultNotification(title, body, onOpen) {
|
|
11
|
+
var container = document.querySelector(".notif-banner-container");
|
|
12
|
+
if (!container) return false;
|
|
13
|
+
var banner = document.createElement("div");
|
|
14
|
+
banner.className = "notif-banner";
|
|
15
|
+
banner.innerHTML =
|
|
16
|
+
'<div class="notif-banner-icon">' + iconHtml("calendar-check") + '</div>' +
|
|
17
|
+
'<div class="notif-banner-body"><div class="notif-banner-project">SCHEDULED TASKS</div>' +
|
|
18
|
+
'<div class="notif-banner-title">' + escapeHtml(title || "Scheduled task finished") + '</div>' +
|
|
19
|
+
(body ? '<div class="notif-banner-text">' + escapeHtml(body) + '</div>' : '') +
|
|
20
|
+
'<div class="notif-banner-actions"><button class="notif-banner-result-open">View result</button></div></div>' +
|
|
21
|
+
'<button class="notif-banner-close" aria-label="Dismiss notification">' + iconHtml("x") + '</button>';
|
|
22
|
+
container.appendChild(banner);
|
|
23
|
+
refreshIcons(banner);
|
|
24
|
+
requestAnimationFrame(function () { banner.classList.add("show"); });
|
|
25
|
+
banner.querySelector(".notif-banner-result-open").addEventListener("click", function (event) {
|
|
26
|
+
event.stopPropagation(); removeBanner(banner); if (typeof onOpen === "function") onOpen();
|
|
27
|
+
});
|
|
28
|
+
banner.querySelector(".notif-banner-close").addEventListener("click", function (event) { event.stopPropagation(); removeBanner(banner); });
|
|
29
|
+
setTimeout(function () { removeBanner(banner); }, 8000);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function scheduledTaskExecutionLabel(record) {
|
|
2
|
+
if (record.activeRun) {
|
|
3
|
+
if (record.activeRun.status === "needs-input") return "Needs input";
|
|
4
|
+
if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
|
|
5
|
+
if (record.activeRun.status === "reviewing") return "Driver reviewing";
|
|
6
|
+
return "Running";
|
|
7
|
+
}
|
|
8
|
+
if (!record.execution) return "Needs setup";
|
|
9
|
+
if (record.readiness && record.readiness.ok === false) return "Unavailable";
|
|
10
|
+
return record.enabled === false ? "Paused" : "Active";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function scheduledTaskOpenRunLabel(record) {
|
|
14
|
+
return record.activeRun && record.activeRun.status === "needs-input" ? "Open blocked run" : "Open run";
|
|
15
|
+
}
|
|
@@ -2,16 +2,14 @@ import { store } from './store.js';
|
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
3
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
4
4
|
import { showToast } from './utils.js';
|
|
5
|
-
import {
|
|
6
|
-
import { closeNotesBrowser } from './sticky-notes-browser.js';
|
|
7
|
-
import { closeFileViewer } from './filebrowser.js';
|
|
8
|
-
import { closeTerminal } from './terminal.js';
|
|
5
|
+
import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
9
6
|
import { humanRecurrence } from './scheduled-task-recurrence.js';
|
|
10
7
|
import { humanScheduleRule } from './scheduled-task-schedule-rule.js';
|
|
11
8
|
import { readExecutionRuntime, renderExecutionRuntime } from './scheduled-task-engine.js';
|
|
12
9
|
import { openDefaultAi } from './default-ai.js';
|
|
13
10
|
import { reconcileScheduledTaskEdit } from './scheduled-task-edit-state.js';
|
|
14
11
|
import { editorFields, fillEditor, readEditor, serverEditorData, validateEditor, handleEditorScheduleChange } from './scheduled-task-form.js';
|
|
12
|
+
import { scheduledTaskExecutionLabel, scheduledTaskOpenRunLabel } from './scheduled-task-presentation.js';
|
|
15
13
|
|
|
16
14
|
export { isStrictCron, humanRecurrence } from './scheduled-task-recurrence.js';
|
|
17
15
|
|
|
@@ -56,18 +54,6 @@ function scheduleLabel(record) {
|
|
|
56
54
|
return "No schedule";
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
function executionLabel(record) {
|
|
60
|
-
if (record.activeRun) {
|
|
61
|
-
if (record.activeRun.status === "needs-input") return "Needs input";
|
|
62
|
-
if (record.activeRun.status === "waiting-worker") return "Waiting for Worker";
|
|
63
|
-
if (record.activeRun.status === "reviewing") return "Driver reviewing";
|
|
64
|
-
return "Running";
|
|
65
|
-
}
|
|
66
|
-
if (!record.execution) return "Needs setup";
|
|
67
|
-
if (record.readiness && record.readiness.ok === false) return "Unavailable";
|
|
68
|
-
return record.enabled === false ? "Paused" : "Active";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
57
|
function ensurePanel() {
|
|
72
58
|
if (panel) return;
|
|
73
59
|
var host = document.getElementById("main-panels");
|
|
@@ -185,7 +171,7 @@ function buildRow(record) {
|
|
|
185
171
|
row.className = "scheduled-task-row" + (record.enabled === false ? " paused" : "");
|
|
186
172
|
row.dataset.taskId = record.id;
|
|
187
173
|
row.setAttribute("role", "listitem");
|
|
188
|
-
var stateLabel =
|
|
174
|
+
var stateLabel = scheduledTaskExecutionLabel(record);
|
|
189
175
|
row.innerHTML =
|
|
190
176
|
'<button class="scheduled-task-summary" type="button" data-action="expand" aria-expanded="' + (expandedId === record.id ? "true" : "false") + '">' +
|
|
191
177
|
'<span class="scheduled-task-state-dot" aria-hidden="true"></span>' +
|
|
@@ -212,12 +198,12 @@ function buildRow(record) {
|
|
|
212
198
|
detail.appendChild(instructions);
|
|
213
199
|
var context = document.createElement("div");
|
|
214
200
|
context.className = "scheduled-task-context";
|
|
215
|
-
var executionState =
|
|
201
|
+
var executionState = scheduledTaskExecutionLabel(record);
|
|
216
202
|
context.textContent = (record.projectTitle || store.get('currentSlug') || "Project") + " · " + executionState + (record.execution ? " · Driver and Split Worker" : " · Choose execution pair in Edit");
|
|
217
203
|
detail.appendChild(context);
|
|
218
204
|
var actions = document.createElement("div");
|
|
219
205
|
actions.className = "scheduled-task-actions";
|
|
220
|
-
actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + '
|
|
206
|
+
actions.innerHTML = '<button type="button" data-action="edit">' + iconHtml("pencil") + 'Edit</button><button type="button" data-action="toggle">' + iconHtml(record.enabled === false ? "play" : "pause") + (record.enabled === false ? "Resume" : "Pause") + '</button><button type="button" data-action="run">' + iconHtml("zap") + 'Run now</button>' + (record.activeRun || record.latestRun ? '<button type="button" data-action="open-run">' + iconHtml("columns-2") + scheduledTaskOpenRunLabel(record) + '</button>' : '') + '<button type="button" data-action="delete" class="danger">' + iconHtml("trash-2") + 'Delete</button>';
|
|
221
207
|
detail.appendChild(actions);
|
|
222
208
|
}
|
|
223
209
|
row.appendChild(detail);
|
|
@@ -271,7 +257,7 @@ function handleClick(event) {
|
|
|
271
257
|
var button = event.target.closest("button[data-action]");
|
|
272
258
|
if (!button) return;
|
|
273
259
|
var action = button.dataset.action;
|
|
274
|
-
if (action === "default-ai") { openDefaultAi(); return; }
|
|
260
|
+
if (action === "default-ai") { event.stopPropagation(); openDefaultAi(); return; }
|
|
275
261
|
if (action === "close") { closeScheduledTasks(); return; }
|
|
276
262
|
if (action === "wide") { applyWindowState(!store.get('scheduledTasksWide'), store.get('scheduledTasksFullscreen')); return; }
|
|
277
263
|
if (action === "fullscreen") { applyWindowState(store.get('scheduledTasksWide'), !store.get('scheduledTasksFullscreen')); return; }
|
|
@@ -331,7 +317,7 @@ export function openScheduledTasks() {
|
|
|
331
317
|
if (!canOpen()) { closeScheduledTasks(); return; }
|
|
332
318
|
ensurePanel();
|
|
333
319
|
if (!panel) return;
|
|
334
|
-
|
|
320
|
+
claimRightWorkbench("scheduled-tasks");
|
|
335
321
|
panel.classList.remove("hidden");
|
|
336
322
|
applyWindowState(store.get('scheduledTasksWide'), false);
|
|
337
323
|
store.set({ scheduledTasksOpen: true });
|
|
@@ -341,6 +327,7 @@ export function openScheduledTasks() {
|
|
|
341
327
|
}
|
|
342
328
|
|
|
343
329
|
export function closeScheduledTasks() {
|
|
330
|
+
releaseRightWorkbench("scheduled-tasks");
|
|
344
331
|
if (panel) panel.classList.add("hidden");
|
|
345
332
|
setCreateMenu(false);
|
|
346
333
|
applyWindowState(store.get('scheduledTasksWide'), false);
|
|
@@ -441,6 +428,7 @@ export function handleScheduledTaskSessionSwitched(msg) {
|
|
|
441
428
|
}
|
|
442
429
|
|
|
443
430
|
export function initScheduledTasks() {
|
|
431
|
+
registerRightWorkbench("scheduled-tasks", closeScheduledTasks);
|
|
444
432
|
var button = document.getElementById("scheduler-btn");
|
|
445
433
|
if (button) button.addEventListener("click", function () { if (isScheduledTasksOpen()) closeScheduledTasks(); else openScheduledTasks(); });
|
|
446
434
|
document.addEventListener("click", handleDocumentClick);
|