clay-server 4.0.0-beta.2 → 4.0.0-beta.21
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/background-task-timing.js +105 -0
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +14 -1
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +21 -1
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +124 -18
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +39 -0
- package/lib/sessions.js +61 -11
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +2 -2
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Snooze control for the update-available banner.
|
|
2
|
+
//
|
|
3
|
+
// Lives outside app-notifications.js so the banner module does not grow
|
|
4
|
+
// further, and because this is a self-contained concern: one compact menu, one
|
|
5
|
+
// message out, one message in.
|
|
6
|
+
//
|
|
7
|
+
// The client decides nothing. It sends an allowlisted duration key and, as a
|
|
8
|
+
// hint only, the browser's UTC offset so "Tomorrow" lands on the user's
|
|
9
|
+
// morning rather than the server's. The deadline, the target version and the
|
|
10
|
+
// user identity are all resolved server-side, so a tampered payload can only
|
|
11
|
+
// ever produce a refusal or a shorter snooze.
|
|
12
|
+
|
|
13
|
+
import { getWs } from './ws-ref.js';
|
|
14
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
15
|
+
|
|
16
|
+
// Must match SNOOZE_OPTIONS in lib/update-snooze.js. The server re-validates
|
|
17
|
+
// every key, so this list is presentation, not policy.
|
|
18
|
+
var SNOOZE_CHOICES = [
|
|
19
|
+
{ id: "3h", label: "3 hours" },
|
|
20
|
+
{ id: "8h", label: "8 hours" },
|
|
21
|
+
{ id: "tomorrow", label: "Tomorrow" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
var openMenu = null;
|
|
25
|
+
var bannerRemover = null;
|
|
26
|
+
|
|
27
|
+
// app-notifications.js owns banner teardown (it runs the exit animation and
|
|
28
|
+
// keeps the clear-all button in sync). Registering the remover here keeps the
|
|
29
|
+
// dependency one-way, so the two modules never form a cycle.
|
|
30
|
+
export function setSnoozeBannerRemover(fn) {
|
|
31
|
+
bannerRemover = typeof fn === "function" ? fn : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isSnoozeChoice(id) {
|
|
35
|
+
for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
|
|
36
|
+
if (SNOOZE_CHOICES[i].id === id) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function closeMenu() {
|
|
42
|
+
if (!openMenu) return;
|
|
43
|
+
var trigger = openMenu.trigger;
|
|
44
|
+
if (openMenu.menu && openMenu.menu.parentNode) {
|
|
45
|
+
openMenu.menu.parentNode.removeChild(openMenu.menu);
|
|
46
|
+
}
|
|
47
|
+
openMenu = null;
|
|
48
|
+
if (trigger) trigger.setAttribute("aria-expanded", "false");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sendSnooze(duration) {
|
|
52
|
+
if (!isSnoozeChoice(duration)) return;
|
|
53
|
+
var ws = getWs();
|
|
54
|
+
if (!ws || ws.readyState !== 1) return;
|
|
55
|
+
// A hint, not an instruction: the server validates the range, computes the
|
|
56
|
+
// instant itself, and clamps the result to its own maximum.
|
|
57
|
+
var offset = -new Date().getTimezoneOffset();
|
|
58
|
+
ws.send(JSON.stringify({ type: "update_snooze", duration: duration, tzOffsetMinutes: offset }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildMenu(trigger) {
|
|
62
|
+
var menu = document.createElement("div");
|
|
63
|
+
menu.className = "notif-banner-snooze-menu";
|
|
64
|
+
menu.setAttribute("role", "menu");
|
|
65
|
+
menu.setAttribute("aria-label", "Snooze this update");
|
|
66
|
+
|
|
67
|
+
for (var i = 0; i < SNOOZE_CHOICES.length; i++) {
|
|
68
|
+
(function (choice) {
|
|
69
|
+
var item = document.createElement("button");
|
|
70
|
+
item.type = "button";
|
|
71
|
+
item.className = "notif-banner-snooze-item";
|
|
72
|
+
item.setAttribute("role", "menuitem");
|
|
73
|
+
item.textContent = choice.label;
|
|
74
|
+
item.addEventListener("click", function (e) {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
closeMenu();
|
|
77
|
+
sendSnooze(choice.id);
|
|
78
|
+
});
|
|
79
|
+
menu.appendChild(item);
|
|
80
|
+
})(SNOOZE_CHOICES[i]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Roving focus with the arrow keys, Escape back to the trigger. Enough for a
|
|
84
|
+
// three-item menu to be usable without a pointer.
|
|
85
|
+
menu.addEventListener("keydown", function (e) {
|
|
86
|
+
var items = menu.querySelectorAll(".notif-banner-snooze-item");
|
|
87
|
+
var index = -1;
|
|
88
|
+
for (var i = 0; i < items.length; i++) {
|
|
89
|
+
if (items[i] === document.activeElement) { index = i; break; }
|
|
90
|
+
}
|
|
91
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
var next = e.key === "ArrowDown" ? index + 1 : index - 1;
|
|
94
|
+
if (next < 0) next = items.length - 1;
|
|
95
|
+
if (next >= items.length) next = 0;
|
|
96
|
+
if (items[next]) items[next].focus();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (e.key === "Escape") {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
closeMenu();
|
|
103
|
+
if (trigger) trigger.focus();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return menu;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Append a compact Snooze control to an existing update banner's action row.
|
|
111
|
+
// Reuses the banner's own button shape rather than introducing a new one.
|
|
112
|
+
export function attachSnoozeControl(actionsEl) {
|
|
113
|
+
if (!actionsEl) return null;
|
|
114
|
+
|
|
115
|
+
var wrap = document.createElement("div");
|
|
116
|
+
wrap.className = "notif-banner-snooze";
|
|
117
|
+
|
|
118
|
+
var trigger = document.createElement("button");
|
|
119
|
+
trigger.type = "button";
|
|
120
|
+
trigger.className = "notif-banner-snooze-btn";
|
|
121
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
122
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
123
|
+
trigger.setAttribute("aria-label", "Snooze this update notification");
|
|
124
|
+
trigger.innerHTML = iconHtml("clock") + "<span>Snooze</span>";
|
|
125
|
+
|
|
126
|
+
trigger.addEventListener("click", function (e) {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
if (openMenu && openMenu.trigger === trigger) {
|
|
129
|
+
closeMenu();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
closeMenu();
|
|
133
|
+
var menu = buildMenu(trigger);
|
|
134
|
+
wrap.appendChild(menu);
|
|
135
|
+
openMenu = { menu: menu, trigger: trigger };
|
|
136
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
137
|
+
var first = menu.querySelector(".notif-banner-snooze-item");
|
|
138
|
+
if (first) first.focus();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
wrap.appendChild(trigger);
|
|
142
|
+
actionsEl.appendChild(wrap);
|
|
143
|
+
refreshIcons();
|
|
144
|
+
return wrap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The server confirms a snooze to every device of that user, so each one drops
|
|
148
|
+
// the banner rather than only the device the user acted on.
|
|
149
|
+
export function handleUpdateSnoozed(msg) {
|
|
150
|
+
closeMenu();
|
|
151
|
+
if (!msg || !msg.ok) return;
|
|
152
|
+
var banner = document.querySelector('.notif-banner[data-notif-id="_update"]');
|
|
153
|
+
if (!banner) return;
|
|
154
|
+
if (msg.version && banner.getAttribute("data-update-version") !== msg.version) return;
|
|
155
|
+
if (bannerRemover) bannerRemover(banner);
|
|
156
|
+
else if (banner.parentNode) banner.parentNode.removeChild(banner);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Dismiss on click-away, blur, or a viewport change so the menu never lingers
|
|
160
|
+
// invisibly. Registered defensively because importing this module must stay
|
|
161
|
+
// side-effect-safe: several suites load the client graph against a partial
|
|
162
|
+
// document/window stub, and a dismiss convenience must not be what breaks them.
|
|
163
|
+
function onGlobal(target, name, fn) {
|
|
164
|
+
if (target && typeof target.addEventListener === "function") target.addEventListener(name, fn);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
onGlobal(typeof document !== "undefined" ? document : null, "click", function () { closeMenu(); });
|
|
168
|
+
onGlobal(typeof window !== "undefined" ? window : null, "blur", function () { closeMenu(); });
|
|
169
|
+
onGlobal(typeof window !== "undefined" ? window : null, "resize", function () { closeMenu(); });
|
|
@@ -10,6 +10,7 @@ import { setSTTLang } from './stt.js';
|
|
|
10
10
|
import { userAvatarUrl } from './avatar.js';
|
|
11
11
|
import { store } from './store.js';
|
|
12
12
|
import { getWs } from './ws-ref.js';
|
|
13
|
+
import { initCapsulePreference, syncCapsulePreference } from './capsule-preference.js';
|
|
13
14
|
|
|
14
15
|
var ctx = null;
|
|
15
16
|
var settingsEl = null;
|
|
@@ -34,6 +35,8 @@ export function initUserSettings(appCtx) {
|
|
|
34
35
|
|
|
35
36
|
if (!settingsEl || !openBtn) return;
|
|
36
37
|
|
|
38
|
+
initCapsulePreference();
|
|
39
|
+
|
|
37
40
|
navItems = settingsEl.querySelectorAll('.us-nav-item');
|
|
38
41
|
sections = settingsEl.querySelectorAll('.us-section');
|
|
39
42
|
pinForm = document.getElementById('us-pin-form');
|
|
@@ -466,6 +469,7 @@ function populateAccount() {
|
|
|
466
469
|
if (matesToggle) matesToggle.checked = matesOn;
|
|
467
470
|
store.set({ projectMateDmsEnabled: matesOn });
|
|
468
471
|
applyProjectMateDmsClass(matesOn);
|
|
472
|
+
syncCapsulePreference(data.capsulesEnabled === true);
|
|
469
473
|
if (data.chatLayout) {
|
|
470
474
|
setChatLayout(data.chatLayout); // update local cache + CSS
|
|
471
475
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Driver-operated Split Worker surface.
|
|
2
|
+
//
|
|
3
|
+
// A configured Split Worker executes work its Driver delegated, so its pane is
|
|
4
|
+
// an execution surface rather than a chat the person types into. This module
|
|
5
|
+
// takes the composer's footprint away and replaces it with one compact,
|
|
6
|
+
// non-interactive status line.
|
|
7
|
+
//
|
|
8
|
+
// What stays, deliberately: the transcript, streaming status, tool and
|
|
9
|
+
// permission progress, scrolling, and the stop control — the human keeps their
|
|
10
|
+
// emergency brake. What goes: the text input, attachments, send, the composer
|
|
11
|
+
// context row, and any Worker-local permission-mode affordance.
|
|
12
|
+
//
|
|
13
|
+
// The role is read from pair state only (`splitGroups` + the displayed
|
|
14
|
+
// session), which is server-broadcast and therefore correct across reconnect,
|
|
15
|
+
// project switch, session switch, dissolve, replacement and role change. There
|
|
16
|
+
// is no second copy of the role and nothing in localStorage. A plain ad-hoc
|
|
17
|
+
// side-by-side split has no pair roles, so it is never locked and keeps its
|
|
18
|
+
// composer.
|
|
19
|
+
//
|
|
20
|
+
// Accessibility: locked controls are removed from the tab order with `disabled`
|
|
21
|
+
// and `tabindex="-1"`, and the composer region is `aria-hidden` while the
|
|
22
|
+
// status line carries the explanation, so a screen reader is told the pane is
|
|
23
|
+
// Driver-controlled instead of finding focusable inputs that do nothing.
|
|
24
|
+
|
|
25
|
+
import { store } from './store.js';
|
|
26
|
+
|
|
27
|
+
var STATUS_ID = "worker-pane-status";
|
|
28
|
+
var LOCK_CLASS = "worker-pane-locked";
|
|
29
|
+
|
|
30
|
+
// Every interactive composer affordance. Queried lazily: a pane iframe and the
|
|
31
|
+
// main window share this markup, and some ids are mobile-only.
|
|
32
|
+
var LOCKED_CONTROL_IDS = [
|
|
33
|
+
"input",
|
|
34
|
+
"send-btn",
|
|
35
|
+
"attach-file-btn",
|
|
36
|
+
"attach-image-btn",
|
|
37
|
+
"input-more-btn",
|
|
38
|
+
"shell-command-btn",
|
|
39
|
+
"stt-btn",
|
|
40
|
+
"schedule-btn",
|
|
41
|
+
"ask-mate-btn",
|
|
42
|
+
"context-sources-add",
|
|
43
|
+
"composer-add-worker-btn",
|
|
44
|
+
"composer-handoff-btn",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function lockedControls() {
|
|
48
|
+
var found = [];
|
|
49
|
+
for (var i = 0; i < LOCKED_CONTROL_IDS.length; i++) {
|
|
50
|
+
var el = document.getElementById(LOCKED_CONTROL_IDS[i]);
|
|
51
|
+
if (el) found.push(el);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The configured Worker of a live pair, for the session actually on screen.
|
|
57
|
+
// Returns false for an ad-hoc split, for the Driver, and for any ordinary
|
|
58
|
+
// session, so the composer is only ever taken away in the one real case.
|
|
59
|
+
export function isDriverOperatedView(state) {
|
|
60
|
+
var snapshot = state || store.snap();
|
|
61
|
+
var sessionId = snapshot.paneMode && snapshot.paneSessionId
|
|
62
|
+
? snapshot.paneSessionId
|
|
63
|
+
: snapshot.activeSessionId;
|
|
64
|
+
if (!sessionId) return false;
|
|
65
|
+
var groups = snapshot.splitGroups || [];
|
|
66
|
+
for (var i = 0; i < groups.length; i++) {
|
|
67
|
+
var pair = groups[i] && groups[i].pair;
|
|
68
|
+
if (!pair) continue;
|
|
69
|
+
if (pair.workerId === sessionId) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureStatusLine(inputArea) {
|
|
75
|
+
var existing = document.getElementById(STATUS_ID);
|
|
76
|
+
if (existing) return existing;
|
|
77
|
+
var line = document.createElement("div");
|
|
78
|
+
line.id = STATUS_ID;
|
|
79
|
+
line.className = "worker-pane-status";
|
|
80
|
+
// A status region, not a live region: it states a standing condition rather
|
|
81
|
+
// than announcing a change, so it should not interrupt.
|
|
82
|
+
line.setAttribute("role", "status");
|
|
83
|
+
var mark = document.createElement("span");
|
|
84
|
+
mark.className = "worker-pane-status-mark";
|
|
85
|
+
mark.setAttribute("aria-hidden", "true");
|
|
86
|
+
var label = document.createElement("span");
|
|
87
|
+
label.className = "worker-pane-status-label";
|
|
88
|
+
label.textContent = "Controlled by Driver";
|
|
89
|
+
line.appendChild(mark);
|
|
90
|
+
line.appendChild(label);
|
|
91
|
+
inputArea.appendChild(line);
|
|
92
|
+
return line;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyLock(locked) {
|
|
96
|
+
var inputArea = document.getElementById("input-area");
|
|
97
|
+
var inputWrapper = document.getElementById("input-wrapper");
|
|
98
|
+
if (!inputArea) return;
|
|
99
|
+
|
|
100
|
+
document.body.classList.toggle(LOCK_CLASS, locked);
|
|
101
|
+
|
|
102
|
+
if (inputWrapper) {
|
|
103
|
+
// Hidden from assistive tech as well as sight; the status line speaks for
|
|
104
|
+
// the region instead.
|
|
105
|
+
if (locked) inputWrapper.setAttribute("aria-hidden", "true");
|
|
106
|
+
else inputWrapper.removeAttribute("aria-hidden");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var controls = lockedControls();
|
|
110
|
+
for (var i = 0; i < controls.length; i++) {
|
|
111
|
+
var el = controls[i];
|
|
112
|
+
if (locked) {
|
|
113
|
+
if (el.dataset.workerLockPrev === undefined) {
|
|
114
|
+
el.dataset.workerLockPrev = el.disabled ? "1" : "0";
|
|
115
|
+
}
|
|
116
|
+
el.disabled = true;
|
|
117
|
+
el.setAttribute("tabindex", "-1");
|
|
118
|
+
} else if (el.dataset.workerLockPrev !== undefined) {
|
|
119
|
+
// Restore exactly what was there before the lock, so an independently
|
|
120
|
+
// disabled send button stays disabled when the pair dissolves.
|
|
121
|
+
el.disabled = el.dataset.workerLockPrev === "1";
|
|
122
|
+
el.removeAttribute("tabindex");
|
|
123
|
+
delete el.dataset.workerLockPrev;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var status = document.getElementById(STATUS_ID);
|
|
128
|
+
if (locked) {
|
|
129
|
+
status = ensureStatusLine(inputArea);
|
|
130
|
+
status.classList.add("visible");
|
|
131
|
+
} else if (status) {
|
|
132
|
+
status.classList.remove("visible");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skip permissions and any other Worker-local permission-mode affordance are
|
|
137
|
+
// the Driver's to set, so they are removed from this pane only. The Driver pane
|
|
138
|
+
// and ordinary sessions are untouched.
|
|
139
|
+
function applyPermissionChrome(locked) {
|
|
140
|
+
var ids = ["skip-perms-pill", "config-chip-wrap"];
|
|
141
|
+
for (var i = 0; i < ids.length; i++) {
|
|
142
|
+
var el = document.getElementById(ids[i]);
|
|
143
|
+
if (!el) continue;
|
|
144
|
+
if (locked) {
|
|
145
|
+
if (el.dataset.workerLockHidden === undefined) {
|
|
146
|
+
el.dataset.workerLockHidden = el.classList.contains("hidden") ? "1" : "0";
|
|
147
|
+
}
|
|
148
|
+
el.classList.add("hidden");
|
|
149
|
+
el.setAttribute("aria-hidden", "true");
|
|
150
|
+
} else if (el.dataset.workerLockHidden !== undefined) {
|
|
151
|
+
if (el.dataset.workerLockHidden === "0") el.classList.remove("hidden");
|
|
152
|
+
el.removeAttribute("aria-hidden");
|
|
153
|
+
delete el.dataset.workerLockHidden;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function syncWorkerPaneLock(state) {
|
|
159
|
+
var locked = isDriverOperatedView(state);
|
|
160
|
+
applyLock(locked);
|
|
161
|
+
applyPermissionChrome(locked);
|
|
162
|
+
return locked;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function initWorkerPaneLock() {
|
|
166
|
+
syncWorkerPaneLock();
|
|
167
|
+
// Pair state is the source of truth, so every input that could change the
|
|
168
|
+
// role re-runs the same resolution: a dissolve, a replacement, a role swap,
|
|
169
|
+
// switching to another session, or a reconnect that reloads the groups.
|
|
170
|
+
store.subscribe(function (state, prev) {
|
|
171
|
+
if (state.splitGroups === prev.splitGroups
|
|
172
|
+
&& state.activeSessionId === prev.activeSessionId
|
|
173
|
+
&& state.paneSessionId === prev.paneSessionId) return;
|
|
174
|
+
syncWorkerPaneLock(state);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function workerProposalSelection(msg) {
|
|
2
|
+
var hasSelection = typeof msg.selectedVendor === "string" ||
|
|
3
|
+
typeof msg.selectedModel === "string" || typeof msg.selectedEffort === "string";
|
|
4
|
+
return {
|
|
5
|
+
selected: hasSelection,
|
|
6
|
+
vendor: hasSelection ? (msg.selectedVendor || "") : (msg.recommendedVendor || ""),
|
|
7
|
+
model: hasSelection ? (msg.selectedModel || "") : (msg.recommendedModel || ""),
|
|
8
|
+
effort: hasSelection ? (msg.selectedEffort || "") : (msg.recommendedEffort || "medium"),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function syncWorkerProposalSelection(msg, sync) {
|
|
13
|
+
var selection = workerProposalSelection(msg);
|
|
14
|
+
if (selection.selected && typeof sync === "function") sync(selection);
|
|
15
|
+
return selection;
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getWs } from './ws-ref.js';
|
|
|
2
2
|
import { addToMessages, scrollToBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
3
3
|
import { effortLevelsFor, effortDisplayName } from './app-panels.js';
|
|
4
4
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { workerProposalSelection, syncWorkerProposalSelection } from './worker-proposal-state.js';
|
|
5
6
|
|
|
6
7
|
function optionValue(entry) {
|
|
7
8
|
if (typeof entry === "string") return entry;
|
|
@@ -27,7 +28,7 @@ function planSteps(plan) {
|
|
|
27
28
|
}).filter(Boolean).slice(0, 8);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function buildVendorSelect(msg) {
|
|
31
|
+
function buildVendorSelect(msg, preferred) {
|
|
31
32
|
var select = document.createElement("select");
|
|
32
33
|
select.className = "worker-proposal-select worker-proposal-vendor";
|
|
33
34
|
var installed = (msg.options && msg.options.installedVendors) || [];
|
|
@@ -37,7 +38,7 @@ function buildVendorSelect(msg) {
|
|
|
37
38
|
option.textContent = VENDOR_NAMES[installed[i]] || installed[i];
|
|
38
39
|
select.appendChild(option);
|
|
39
40
|
}
|
|
40
|
-
if (installed.indexOf(
|
|
41
|
+
if (installed.indexOf(preferred) !== -1) select.value = preferred;
|
|
41
42
|
return select;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -98,16 +99,30 @@ function statusLabel(status) {
|
|
|
98
99
|
if (status === "interrupted") return "Interrupted";
|
|
99
100
|
if (status === "declined") return "Continuing here";
|
|
100
101
|
if (status === "error") return "Needs attention";
|
|
101
|
-
return "
|
|
102
|
+
return "Awaiting your choice";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decisionLabel(status, autoAccepted) {
|
|
106
|
+
if (autoAccepted) return "Driver recommendation auto-accepted under Full auto";
|
|
107
|
+
if (status === "pending") return "Runtime choice remains with you";
|
|
108
|
+
if (status === "declined") return "Runtime proposal declined by user";
|
|
109
|
+
return "Runtime configuration accepted by user";
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function applyState(card, msg) {
|
|
105
113
|
var status = msg.status || "pending";
|
|
106
114
|
card.dataset.status = status;
|
|
107
|
-
if (msg.autoApproved) card.dataset.
|
|
108
|
-
|
|
115
|
+
if (msg.autoAccepted || msg.autoApproved) card.dataset.autoAccepted = "true";
|
|
116
|
+
if (msg.autoAccepted === false || msg.autoApproved === false) card.dataset.autoAccepted = "false";
|
|
117
|
+
var autoAccepted = card.dataset.autoAccepted === "true";
|
|
118
|
+
syncWorkerProposalSelection(msg, card._syncWorkerProposalSelection);
|
|
109
119
|
var badge = card.querySelector(".worker-proposal-status");
|
|
110
|
-
if (badge) badge.textContent = statusLabel(status) + (
|
|
120
|
+
if (badge) badge.textContent = statusLabel(status) + (autoAccepted ? " · auto-accepted" : "");
|
|
121
|
+
var decision = card.querySelector(".worker-proposal-decision");
|
|
122
|
+
if (decision) {
|
|
123
|
+
decision.textContent = decisionLabel(status, autoAccepted);
|
|
124
|
+
decision.classList.toggle("automatic", autoAccepted);
|
|
125
|
+
}
|
|
111
126
|
var error = card.querySelector(".worker-proposal-error");
|
|
112
127
|
if (error) {
|
|
113
128
|
error.textContent = msg.error || "";
|
|
@@ -120,7 +135,7 @@ function applyState(card, msg) {
|
|
|
120
135
|
}
|
|
121
136
|
setControlsDisabled(card, status !== "pending");
|
|
122
137
|
var actions = card.querySelector(".worker-proposal-actions");
|
|
123
|
-
if (actions) actions.classList.toggle("hidden",
|
|
138
|
+
if (actions) actions.classList.toggle("hidden", autoAccepted);
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
function sendDecision(card, accepted) {
|
|
@@ -150,10 +165,12 @@ export function renderWorkerProposal(msg) {
|
|
|
150
165
|
card.className = "worker-proposal-card";
|
|
151
166
|
card.dataset.proposalId = msg.proposalId;
|
|
152
167
|
card.dataset.status = msg.status || "pending";
|
|
168
|
+
var runtimeSelection = workerProposalSelection(msg);
|
|
153
169
|
|
|
154
170
|
var header = document.createElement("div");
|
|
155
171
|
header.className = "worker-proposal-header";
|
|
156
|
-
|
|
172
|
+
var replacing = msg.action === "replace";
|
|
173
|
+
header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / SPLIT WORKER</span><strong>' + (replacing ? "Configure a replacement Split Worker" : "Configure a Split Worker") + '</strong></div><span class="worker-proposal-status"></span>';
|
|
157
174
|
card.appendChild(header);
|
|
158
175
|
|
|
159
176
|
var summary = document.createElement("p");
|
|
@@ -173,12 +190,24 @@ export function renderWorkerProposal(msg) {
|
|
|
173
190
|
card.appendChild(plan);
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
if (msg.recommendationRationale) {
|
|
194
|
+
var rationale = document.createElement("div");
|
|
195
|
+
rationale.className = "worker-proposal-rationale";
|
|
196
|
+
var rationaleLabel = document.createElement("span");
|
|
197
|
+
rationaleLabel.textContent = "Driver's recommendation rationale";
|
|
198
|
+
var rationaleText = document.createElement("p");
|
|
199
|
+
rationaleText.textContent = msg.recommendationRationale;
|
|
200
|
+
rationale.appendChild(rationaleLabel);
|
|
201
|
+
rationale.appendChild(rationaleText);
|
|
202
|
+
card.appendChild(rationale);
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
var config = document.createElement("div");
|
|
177
206
|
config.className = "worker-proposal-config";
|
|
178
207
|
var vendorField = document.createElement("label");
|
|
179
208
|
vendorField.className = "worker-proposal-field worker-proposal-vendor-field";
|
|
180
209
|
vendorField.innerHTML = '<span>Worker</span><span class="worker-proposal-vendor-control"><img alt=""></span>';
|
|
181
|
-
var vendorSelect = buildVendorSelect(msg);
|
|
210
|
+
var vendorSelect = buildVendorSelect(msg, runtimeSelection.vendor);
|
|
182
211
|
vendorField.querySelector(".worker-proposal-vendor-control").appendChild(vendorSelect);
|
|
183
212
|
config.appendChild(vendorField);
|
|
184
213
|
var modelField = document.createElement("label");
|
|
@@ -194,6 +223,10 @@ export function renderWorkerProposal(msg) {
|
|
|
194
223
|
config.appendChild(effortField);
|
|
195
224
|
card.appendChild(config);
|
|
196
225
|
|
|
226
|
+
var decision = document.createElement("div");
|
|
227
|
+
decision.className = "worker-proposal-decision";
|
|
228
|
+
card.appendChild(decision);
|
|
229
|
+
|
|
197
230
|
function syncVendor(preferredModel, preferredEffort) {
|
|
198
231
|
var vendor = vendorSelect.value;
|
|
199
232
|
var avatar = vendorField.querySelector("img");
|
|
@@ -201,12 +234,16 @@ export function renderWorkerProposal(msg) {
|
|
|
201
234
|
fillModels(modelSelect, vendor, msg, preferredModel);
|
|
202
235
|
fillEffort(card, vendor, modelSelect.value, msg, preferredEffort);
|
|
203
236
|
}
|
|
237
|
+
card._syncWorkerProposalSelection = function (selection) {
|
|
238
|
+
if (selection.vendor && vendorSelect.value !== selection.vendor) vendorSelect.value = selection.vendor;
|
|
239
|
+
syncVendor(selection.model, selection.effort);
|
|
240
|
+
};
|
|
204
241
|
vendorSelect.addEventListener("change", function () { syncVendor("", "medium"); });
|
|
205
242
|
modelSelect.addEventListener("change", function () {
|
|
206
243
|
var active = card.querySelector(".worker-proposal-effort-btn.active");
|
|
207
244
|
fillEffort(card, vendorSelect.value, modelSelect.value, msg, active ? active.dataset.effort : "medium");
|
|
208
245
|
});
|
|
209
|
-
syncVendor(
|
|
246
|
+
syncVendor(runtimeSelection.model, runtimeSelection.effort);
|
|
210
247
|
|
|
211
248
|
var error = document.createElement("div");
|
|
212
249
|
error.className = "worker-proposal-error hidden";
|
|
@@ -220,12 +257,12 @@ export function renderWorkerProposal(msg) {
|
|
|
220
257
|
var decline = document.createElement("button");
|
|
221
258
|
decline.type = "button";
|
|
222
259
|
decline.className = "worker-proposal-action secondary";
|
|
223
|
-
decline.textContent = "Continue here";
|
|
260
|
+
decline.textContent = replacing ? "Keep current Worker" : "Continue here";
|
|
224
261
|
decline.addEventListener("click", function () { sendDecision(card, false); });
|
|
225
262
|
var accept = document.createElement("button");
|
|
226
263
|
accept.type = "button";
|
|
227
264
|
accept.className = "worker-proposal-action primary";
|
|
228
|
-
accept.innerHTML = iconHtml("panel-right-open") + "<span>Run with Split Worker</span>";
|
|
265
|
+
accept.innerHTML = iconHtml("panel-right-open") + "<span>" + (replacing ? "Replace Split Worker" : "Run with Split Worker") + "</span>";
|
|
229
266
|
accept.addEventListener("click", function () { sendDecision(card, true); });
|
|
230
267
|
actions.appendChild(decline);
|
|
231
268
|
actions.appendChild(accept);
|
package/lib/public/style.css
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
@import url("css/input.css?v=20260828-composer-depth1");
|
|
15
15
|
@import url("css/filebrowser.css?v=20260829-terminal-canvas1");
|
|
16
16
|
@import url("css/git-panel.css");
|
|
17
|
+
@import url("css/git-placard.css");
|
|
17
18
|
@import url("css/diff.css");
|
|
18
19
|
@import url("css/highlight.css");
|
|
19
20
|
@import url("css/server-settings.css");
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
@import url("css/mobile-nav.css?v=20260407");
|
|
23
24
|
@import url("css/loop.css");
|
|
24
25
|
@import url("css/scheduler.css");
|
|
26
|
+
@import url("css/project-logs.css");
|
|
25
27
|
@import url("css/scheduler-modal.css");
|
|
26
28
|
@import url("css/home-hub.css");
|
|
27
29
|
@import url("css/home-sidebar.css");
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
@import url("css/session-search.css");
|
|
40
42
|
@import url("css/tooltip.css");
|
|
41
43
|
@import url("css/mates.css?v=20260407");
|
|
42
|
-
@import url("css/command-palette.css?v=
|
|
44
|
+
@import url("css/command-palette.css?v=20260908-log-links2");
|
|
43
45
|
@import url("css/mention.css");
|
|
44
46
|
@import url("css/debate.css");
|
|
45
47
|
@import url("css/home-debate-planning.css");
|