🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
Requirement 001 decisions 52 to 58 (operator 2026-11-09): - Radio spot: the Agents toolbar radio button expands inline with the one composer (every channel, post and call recipients, Record, Send), a gear with the radio settings (tones, squelch, read aloud, moved out of the general settings) and Cancel mayday, which leaves the toolbar; the Radio tab keeps speakers, stations and the log plus a jump to the spot. - Day/Night menu: the badge and Drain, Suspend and Resume only; each explanation is the button's tooltip. - Tasks toolbar: views, filter, estimation, sort and business value in one wrapping row with a Request choices dropdown; the status chips stay the second row. - Burndown and burnup: a trend selector (off, linear regression, velocity over N days) stored in the hub settings, the trend lines and the forecast date on the chart, and a tooltip per point with the work packages that started, ended or joined there. - ETA: every list row shows its ETA (end when done, else start or now plus the forecast length behind its predecessors), sharing one predecessor walk with the Gantt. - Tasks 3D: a sixth view on the 3D engine with the dependency edges in the Gantt colours, layouts Force and Time. - Steady 3D camera: the view fits once per graph build, refreshes keep the operator's camera. - e2e: Firefox runs with its profile as cwd, so its crashreporter no longer drops core dumps into the repository. - A forecast beyond the last second a Date can hold is no forecast, so a nearly flat regression no longer aborts the Tasks render. Evidence: make test Ran 717 OK, ruff clean, markdownlint 0 errors; e2e 30 of 30 OK in Chromium and Firefox at 1920, 1366 and 390 px; MEDUSA 0 findings after one skeptic-accepted lexical EXA006. 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
7.5 KiB
JavaScript
155 lines
7.5 KiB
JavaScript
function paletteItems() {
|
||
return [
|
||
...Nav.TABS.flatMap(([id, label], index) => [{label: `View: ${label}`, hint: String(index + 1), run: () => go(id, null)}, ...(Nav.SUBS[id] || []).map(([sub, name]) => ({label: `View: ${label} › ${name}`, hint: Nav.hashOf(sub, null), run: () => go(sub, null)}))]),
|
||
...["agents", "artifacts", "tasks"].flatMap((screen) => Nav.viewsOf(screen).map(([id, name]) => ({label: `${Nav.TABS.find(([tab]) => tab === screen)[1]}: ${name}`, hint: Nav.hashOf(screen, id), run: () => go(screen, id)}))),
|
||
...Object.entries(state.views).map(([id, view]) => ({label: `Stream: ${view.label}`, hint: "Category", run: () => useView(id)})),
|
||
...userViews.map((view) => ({label: `Stream: ${view.name}`, hint: "saved view", run: () => useSaved(view)})),
|
||
...agentNames().map((name) => ({label: name, hint: "Agent", face: name, run: () => openAgent(name)})),
|
||
{label: "Radio …", hint: "Post, Call, Newsletter, Broadcast, Pan-Pan, Security, Mayday; the gear holds tones and read aloud", run: openRadio},
|
||
...ACTIONS.map(([id, label]) => ({label, hint: id === "clear-mayday" ? "Mayday" : "Day/Night", run: () => confirmAct(id)})),
|
||
{label: "Priority …", hint: "agent order in the hierarchy", run: openAgentOrder},
|
||
{label: "Firewall …", hint: "automatic grants", run: () => go("rules", null)},
|
||
{label: "Settings …", hint: "Ambience, location, colors, music, notifications, internet radio", run: settingsSheet},
|
||
{label: settings.music.on ? "Music off" : "Music on", hint: "Ambience", run: toggleMusic},
|
||
{label: space.look ? "Leave the sky" : "Look at the sky", hint: "h", run: () => skyLook(!space.look)},
|
||
{label: "Toggle control room", hint: "Ambience", run: toggleLeitstand},
|
||
{label: "Enable notifications", hint: "Ambience", run: askNotify},
|
||
{label: "Keyboard shortcuts", hint: "?", run: helpSheet},
|
||
];
|
||
}
|
||
|
||
function drawPalette() {
|
||
if (palette.plan) {
|
||
return;
|
||
}
|
||
const raw = palette.input.value.trim();
|
||
const q = raw.toLowerCase();
|
||
const items = paletteItems().filter((item) => !q || item.label.toLowerCase().includes(q) || item.hint.toLowerCase().includes(q));
|
||
if (raw) {
|
||
items.push(state.llm
|
||
? {label: `AI command: “${raw}”`, hint: "Tab", keep: true, run: () => askModel(raw)}
|
||
: {label: "AI command is off", hint: "set KVB_ORCHESTRATOR_LLM_URL and KVB_ORCHESTRATOR_LLM_MODEL in orchestrator/.env", keep: true, run: () => toast("AI command is off", ["Set KVB_ORCHESTRATOR_LLM_URL, KVB_ORCHESTRATOR_LLM_MODEL and KVB_ORCHESTRATOR_LLM_KEY_FILE in orchestrator/.env and reinstall."], false)});
|
||
}
|
||
palette.items = items;
|
||
palette.index = Math.min(palette.index, Math.max(items.length - 1, 0));
|
||
palette.list.replaceChildren(...items.map((item, index) => h("li", {role: "option", "aria-selected": String(index === palette.index), on: {
|
||
click: () => runPalette(index),
|
||
mousemove: () => {
|
||
if (palette.index !== index) {
|
||
palette.index = index;
|
||
drawPalette();
|
||
}
|
||
},
|
||
}}, h("span", {class: item.face ? "who" : null}, item.face ? face(item.face, false) : null, item.label), h("span", {class: "muted"}, item.hint))));
|
||
const current = palette.list.children[palette.index];
|
||
if (current) {
|
||
current.scrollIntoView({block: "nearest"});
|
||
}
|
||
}
|
||
|
||
function runPalette(index) {
|
||
const item = palette.items[index];
|
||
if (!item) {
|
||
return;
|
||
}
|
||
if (!item.keep) {
|
||
closePalette();
|
||
}
|
||
item.run();
|
||
}
|
||
|
||
async function askModel(text) {
|
||
palette.plan = {actions: [], rejected: [], busy: true};
|
||
palette.list.replaceChildren(h("li", {}, h("span", {class: "muted"}, "Asking the model … (only the command, ids and titles leave the hub)")));
|
||
const result = await sendJson("/api/nl", {text});
|
||
if (!palette.plan) {
|
||
return;
|
||
}
|
||
if (!result.ok || !result.data) {
|
||
palette.plan = null;
|
||
palette.list.replaceChildren(h("li", {}, h("span", {}, `AI command failed: ${result.status} ${result.text}`.trim())));
|
||
return;
|
||
}
|
||
palette.plan = result.data;
|
||
const actions = result.data.actions.map((action) => h("pre", {}, action.summary));
|
||
const rejected = result.data.rejected.map((entry) => h("p", {class: "muted"}, `discarded: ${entry.reason} · ${entry.action}`));
|
||
palette.list.replaceChildren(h("li", {class: "palette-plan"},
|
||
actions.length ? actions : h("p", {class: "muted"}, "The model proposed no valid action."),
|
||
rejected,
|
||
h("p", {class: "muted"}, actions.length ? "Enter runs exactly these actions · Esc cancels" : "Esc closes")));
|
||
}
|
||
|
||
async function executePlan(plan) {
|
||
for (const action of plan.actions) {
|
||
const args = action.args;
|
||
if (action.name === "approve" || action.name === "reject" || action.name === "choose") {
|
||
await submit(args.items, action.summary.split("\n")[0]);
|
||
} else if (action.name === "snooze" || action.name === "pin") {
|
||
for (const key of args.keys) {
|
||
const item = state.inbox.find((entry) => entry.kind === key.kind && entry.id === key.id);
|
||
if (item) {
|
||
await mark(item, action.name === "snooze" ? nowSeconds() + args.hours * 3600 : item.until, action.name === "pin" ? true : item.pinned);
|
||
}
|
||
}
|
||
} else if (action.name === "view") {
|
||
useView(args.name);
|
||
} else if (action.name === "open") {
|
||
go(args.view, null);
|
||
} else if (action.name === "message") {
|
||
await message(args.to, args.text, "", "post");
|
||
} else if (action.name === "filter") {
|
||
filter.q = args.q;
|
||
if (ui.screen === "stream" && stream.dom) {
|
||
stream.dom.search.value = args.q;
|
||
refilter();
|
||
} else {
|
||
go("stream", null);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function openPalette() {
|
||
const box = document.getElementById("palette");
|
||
palette.input = h("input", {type: "text", placeholder: "View, agent, action, or a command in words …", "aria-label": "Command", role: "combobox", "aria-expanded": "true"});
|
||
palette.list = h("ul", {class: "palette-list", role: "listbox"});
|
||
palette.index = 0;
|
||
palette.plan = null;
|
||
palette.input.addEventListener("input", () => {
|
||
palette.index = 0;
|
||
palette.plan = null;
|
||
drawPalette();
|
||
});
|
||
palette.input.addEventListener("keydown", (event) => {
|
||
if (palette.plan && event.key === "Enter") {
|
||
event.preventDefault();
|
||
const plan = palette.plan;
|
||
if (!plan.busy && plan.actions.length) {
|
||
closePalette();
|
||
executePlan(plan);
|
||
}
|
||
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||
event.preventDefault();
|
||
const size = Math.max(palette.items.length, 1);
|
||
palette.index = (palette.index + (event.key === "ArrowDown" ? 1 : size - 1)) % size;
|
||
drawPalette();
|
||
} else if (event.key === "Tab" && palette.input.value.trim() && !palette.plan) {
|
||
event.preventDefault();
|
||
palette.index = palette.items.length - 1;
|
||
runPalette(palette.index);
|
||
} else if (event.key === "Enter") {
|
||
event.preventDefault();
|
||
runPalette(palette.index);
|
||
}
|
||
});
|
||
box.replaceChildren(h("div", {class: "palette-box", role: "dialog", "aria-label": "Commands"}, h("div", {class: "palette-row"}, palette.input, micButton(palette.input)), palette.list));
|
||
box.hidden = false;
|
||
drawPalette();
|
||
palette.input.focus();
|
||
}
|
||
|
||
function closePalette() {
|
||
document.getElementById("palette").hidden = true;
|
||
palette.plan = null;
|
||
}
|