toolroll 0.9.86 → 0.9.88
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/CHANGELOG.md +86 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +722 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +31 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +38 -4
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +23 -0
- package/dist/release.js +46 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +24 -12
- package/dist/server/flows.js +64 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +70 -0
- package/dist/server/kit.d.ts +419 -0
- package/dist/server/kit.js +369 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +22 -190
- package/dist/server/render-pages.js +123 -575
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
package/dist/server/chat.js
CHANGED
|
@@ -38,7 +38,7 @@ import { WORK_INDEX_MAX_LIMIT, workIndexPage } from "../work-index.js";
|
|
|
38
38
|
import { html, htmlString, postForm, reauth } from "../html.js";
|
|
39
39
|
import { requestContext } from "./request-context.js";
|
|
40
40
|
import { refuse, screen, whenTime } from "./chrome.js";
|
|
41
|
-
import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
|
|
41
|
+
import { chatResultHref, chatReturnWithLatest, chatRefusal, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
|
|
42
42
|
import { chatAckPage, chatPage, NOT_STARTED_SESSION, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
|
|
43
43
|
import {} from "./render-pages.js";
|
|
44
44
|
import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
|
|
@@ -527,18 +527,18 @@ export function createChatHandlers(runtime) {
|
|
|
527
527
|
return refuse(response, who, 403, "the sandbox never pushes");
|
|
528
528
|
const password = body.get("token") ?? "";
|
|
529
529
|
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
530
|
-
return redirect(response, `/settings?said=${encodeURIComponent("enrolling this device takes your password, typed again")}`);
|
|
530
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent("enrolling this device takes your password, typed again")}#this-device`);
|
|
531
531
|
}
|
|
532
532
|
const endpoint = body.get("endpoint") ?? "";
|
|
533
533
|
const p256dh = body.get("p256dh") ?? "";
|
|
534
534
|
const auth = body.get("auth") ?? "";
|
|
535
535
|
const checked = validatePushEndpoint(endpoint);
|
|
536
536
|
if (!checked.ok)
|
|
537
|
-
return redirect(response, `/settings?said=${encodeURIComponent(checked.problem)}`);
|
|
537
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent(checked.problem)}#this-device`);
|
|
538
538
|
const p256dhBytes = Buffer.from(p256dh, "base64url");
|
|
539
539
|
const authBytes = Buffer.from(auth, "base64url");
|
|
540
540
|
if (p256dhBytes.length !== 65 || p256dhBytes[0] !== 4 || authBytes.length !== 16) {
|
|
541
|
-
return redirect(response, `/settings?said=${encodeURIComponent("that subscription's keys are not the shape a browser mints")}`);
|
|
541
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent("that subscription's keys are not the shape a browser mints")}#this-device`);
|
|
542
542
|
}
|
|
543
543
|
const keys = loadOrCreateVapidKeys(dirname(options.telegramTokenFile ?? "."));
|
|
544
544
|
const generation = store.approverGeneration(who.name) ?? 1;
|
|
@@ -552,9 +552,9 @@ export function createChatHandlers(runtime) {
|
|
|
552
552
|
vapidFingerprint: keys.fingerprint,
|
|
553
553
|
}, clock());
|
|
554
554
|
if (!enrolled.ok) {
|
|
555
|
-
return redirect(response, `/settings?said=${encodeURIComponent(enrolled.reason === "approver-cap" ? "five devices per person — remove one first" : "twenty devices per installation — remove one first")}`);
|
|
555
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent(enrolled.reason === "approver-cap" ? "five devices per person — remove one first" : "twenty devices per installation — remove one first")}#this-device`);
|
|
556
556
|
}
|
|
557
|
-
return redirect(response, `/settings?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}`);
|
|
557
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}#this-device`);
|
|
558
558
|
}
|
|
559
559
|
async function pushRemove(ctx) {
|
|
560
560
|
const { who, response, posted } = ctx;
|
|
@@ -564,9 +564,9 @@ export function createChatHandlers(runtime) {
|
|
|
564
564
|
const id = Number(body.get("id") ?? "");
|
|
565
565
|
const mine = store.listPushSubscriptions(who.name).find(one => one.id === id && one.retiredAt === null);
|
|
566
566
|
if (mine === undefined)
|
|
567
|
-
return redirect(response, `/settings?said=${encodeURIComponent("that device is not yours to remove, or it is already gone")}`);
|
|
567
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent("that device is not yours to remove, or it is already gone")}#this-device`);
|
|
568
568
|
store.retirePushSubscription(mine.id, `removed by ${who.name}`, clock());
|
|
569
|
-
return redirect(response, `/settings?said=${encodeURIComponent("removed — that device stops receiving pushes")}`);
|
|
569
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent("removed — that device stops receiving pushes")}#this-device`);
|
|
570
570
|
}
|
|
571
571
|
async function phoneDismiss(ctx) {
|
|
572
572
|
const { who, response, now, posted } = ctx;
|
|
@@ -587,7 +587,7 @@ export function createChatHandlers(runtime) {
|
|
|
587
587
|
const back = body.get("return") === "/settings/lead" ? "/settings/lead" : safeChatReturn(body.get("return"));
|
|
588
588
|
const password = body.get("token") ?? "";
|
|
589
589
|
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
590
|
-
return redirect(response,
|
|
590
|
+
return redirect(response, chatRefusal(back, "configuring chat spend takes your password, typed again"));
|
|
591
591
|
}
|
|
592
592
|
if (body.get("off") === "1") {
|
|
593
593
|
store.clearChatConfig();
|
|
@@ -604,12 +604,12 @@ export function createChatHandlers(runtime) {
|
|
|
604
604
|
const weekly = Number(weeklyText);
|
|
605
605
|
const daily = (body.get("daily-turns") ?? "").trim() === "" ? 50 : Number(body.get("daily-turns"));
|
|
606
606
|
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription") {
|
|
607
|
-
return redirect(response,
|
|
607
|
+
return redirect(response, chatRefusal(back, "pick a chat provider"));
|
|
608
608
|
}
|
|
609
609
|
const subscription = isSubscriptionChatProvider(provider);
|
|
610
610
|
const model = requestedModel === "" && subscription ? "default" : requestedModel;
|
|
611
611
|
if (!validModelId(model)) {
|
|
612
|
-
return redirect(response,
|
|
612
|
+
return redirect(response, chatRefusal(back, "the model id must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes"));
|
|
613
613
|
}
|
|
614
614
|
// The key, when pasted, is stored FIRST (0600 file, Telegram-token
|
|
615
615
|
// precedent) so the catalog fetch below can already use it. It never
|
|
@@ -617,10 +617,10 @@ export function createChatHandlers(runtime) {
|
|
|
617
617
|
const pastedKey = (body.get("key") ?? "").trim();
|
|
618
618
|
if (pastedKey !== "") {
|
|
619
619
|
if (subscription)
|
|
620
|
-
return redirect(response,
|
|
620
|
+
return redirect(response, chatRefusal(back, "subscription chat uses the CLI's cached login — do not paste an API key"));
|
|
621
621
|
const stored = storeChatKey(provider, pastedKey);
|
|
622
622
|
if (!stored.ok)
|
|
623
|
-
return redirect(response,
|
|
623
|
+
return redirect(response, chatRefusal(back, stored.message));
|
|
624
624
|
}
|
|
625
625
|
// The pin: anthropic models come from the compiled table; openrouter
|
|
626
626
|
// models come from OpenRouter's OWN catalog, priced by the authority
|
|
@@ -635,13 +635,13 @@ export function createChatHandlers(runtime) {
|
|
|
635
635
|
pin = hit.price;
|
|
636
636
|
}
|
|
637
637
|
if (!subscription && pin === null) {
|
|
638
|
-
return redirect(response,
|
|
638
|
+
return redirect(response, chatRefusal(back, provider === "openrouter-api" ? "that model is not in OpenRouter's catalog (or the catalog is unreachable) — chat cannot reserve spend it cannot bound" : "that model has no pinned price — chat cannot reserve spend it cannot bound"));
|
|
639
639
|
}
|
|
640
640
|
if (!subscription && (!Number.isFinite(weekly) || weekly <= 0)) {
|
|
641
|
-
return redirect(response,
|
|
641
|
+
return redirect(response, chatRefusal(back, "the weekly ceiling is a positive dollar amount — chat without one is unbounded, not configured"));
|
|
642
642
|
}
|
|
643
643
|
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000) {
|
|
644
|
-
return redirect(response,
|
|
644
|
+
return redirect(response, chatRefusal(back, "daily turns is a whole number between 1 and 1000"));
|
|
645
645
|
}
|
|
646
646
|
store.setChatConfig({
|
|
647
647
|
provider,
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
|
|
|
44
44
|
* React workspace, the pre-script fallback) reads these.
|
|
45
45
|
*
|
|
46
46
|
* The palette above, in light, dark and pinned themes, is Signal's generated
|
|
47
|
-
* tokens.css (src/design,
|
|
47
|
+
* tokens.css (src/design, generated by youiyoux from the toolroll System, src/design/system); the
|
|
48
48
|
* console's own rules follow in console.css, shipped beside this module.
|
|
49
49
|
* Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
|
|
50
50
|
* workspace's stylesheet loads after this one and never redefines a token, so
|
package/dist/server/chrome.js
CHANGED
|
@@ -34,6 +34,7 @@ import { RECIPE_CSS } from "../recipe-ui.js";
|
|
|
34
34
|
import { REQUEST_LIMITS_CSS } from "../request-budget-ui.js";
|
|
35
35
|
import { RETENTION_CSS } from "../retention-ui.js";
|
|
36
36
|
import { crumbsHtml, SETTINGS_KIT_CSS } from "../settings-kit.js";
|
|
37
|
+
import { acts as kitActs, btn as kitBtn, doc as kitDoc, docHeader as kitDocHeader, notFound as kitNotFound, notice as kitNotice, outPage } from "./kit.js";
|
|
37
38
|
import { SKILLS_CSS } from "../skills-ui.js";
|
|
38
39
|
import { SPEND_CSS } from "../spend-ui.js";
|
|
39
40
|
import { SSO_CSS } from "../sso-ui.js";
|
|
@@ -95,17 +96,23 @@ export function refuse(response, who, status, message, backHref = "/") {
|
|
|
95
96
|
if (who === null || who.via === "bearer") {
|
|
96
97
|
return respond(response, status, "text/plain; charset=utf-8", message);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// Kit pages (Wave E): a missing page is plain news, not an error, with two ways on; a refusal says what was refused
|
|
100
|
+
// where it happened and leads back.
|
|
101
|
+
const path = new URL(requestContext.getStore()?.returnTo ?? "/", "http://standing-orders.local").pathname;
|
|
102
|
+
const generic = /^there'?s no page at this address\.?$/i.test(message.replace("\u2019", "'"));
|
|
103
|
+
// The words are the caller's exactly (older callers wrote lower-case fragments; k-refusal capitalises the first letter).
|
|
104
|
+
const body = html `<div class="k-refusal">${status === 404
|
|
105
|
+
? kitNotFound(path, generic ? {} : { title: message, text: "Home has what needs you; Tasks has every task." })
|
|
106
|
+
: kitDoc(kitDocHeader({ title: "Request refused", back: [backHref, "Back"] }), kitNotice(message), kitActs(kitBtn("Go back", { href: backHref })))}</div>`;
|
|
104
107
|
// A signed-in browser keeps the workspace around it: the same navigation as every page.
|
|
105
108
|
const framed = requestContext.getStore()?.refusal;
|
|
106
109
|
if (framed !== undefined && who.via === "cookie")
|
|
107
110
|
return framed(response, status, body);
|
|
108
|
-
return page(response, status, shell("refused",
|
|
111
|
+
return page(response, status, shell(status === 404 ? "Not found" : "Request refused", outPage({
|
|
112
|
+
brand: BRAND_HTML, title: status === 404 ? "Not found" : "Request refused",
|
|
113
|
+
body: html `<div class="k-refusal">${kitNotice(status === 404 && generic ? `There\u2019s no page at ${path}.` : message)}</div>`,
|
|
114
|
+
foot: html `<a class="k-link" href="${status === 404 ? "/" : backHref}">${status === 404 ? "Go to Home" : "Go back"}</a>`,
|
|
115
|
+
})));
|
|
109
116
|
}
|
|
110
117
|
/** The demo's one-line promise, on every page. */
|
|
111
118
|
export const DEMO_BANNER = `Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends. For your own project, run ${START_COMMAND} in its folder.`;
|
|
@@ -129,13 +136,16 @@ export const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model o
|
|
|
129
136
|
* React workspace, the pre-script fallback) reads these.
|
|
130
137
|
*
|
|
131
138
|
* The palette above, in light, dark and pinned themes, is Signal's generated
|
|
132
|
-
* tokens.css (src/design,
|
|
139
|
+
* tokens.css (src/design, generated by youiyoux from the toolroll System, src/design/system); the
|
|
133
140
|
* console's own rules follow in console.css, shipped beside this module.
|
|
134
141
|
* Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
|
|
135
142
|
* workspace's stylesheet loads after this one and never redefines a token, so
|
|
136
143
|
* a person's accent (accentHead) still wins over the generated default. */
|
|
137
144
|
export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tokens.css"), "utf8") +
|
|
138
|
-
readFileSync(join(import.meta.dirname, "console.css"), "utf8")
|
|
145
|
+
readFileSync(join(import.meta.dirname, "console.css"), "utf8") +
|
|
146
|
+
// The revamp kit (src/design/KIT.md): k- classes only, after the console's rules, so the kit's markup from
|
|
147
|
+
// server pages (server/kit.ts) and React views (browser/kit/) looks the same everywhere.
|
|
148
|
+
readFileSync(join(import.meta.dirname, "..", "design", "kit.css"), "utf8");
|
|
139
149
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
140
150
|
export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
|
|
141
151
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
package/dist/server/console.css
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
}
|
|
33
33
|
/* Signal's colour, shadow, radius, type and spacing tokens (--so-*) are
|
|
34
34
|
* generated by youi into src/design/tokens.css, light and dark, and the
|
|
35
|
-
* server prepends that sheet to this one (chrome.ts STYLE). Edit them in
|
|
36
|
-
* design
|
|
35
|
+
* server prepends that sheet to this one (chrome.ts STYLE). Edit them in
|
|
36
|
+
* src/design/system/tokens and run youiyoux sync, never here. The device decides the scheme unless the
|
|
37
37
|
* person pins one (html[data-theme], set from their cookie). What follows are
|
|
38
38
|
* the console's older names: views onto those tokens, never a second ramp. */
|
|
39
39
|
:root {
|
|
@@ -265,16 +265,26 @@
|
|
|
265
265
|
.project-card .project-name:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: .375rem; }
|
|
266
266
|
|
|
267
267
|
label { display: block; font-size: 0.8125rem; font-weight: 500; margin: .75rem 0 0; color: var(--foreground); }
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
/* The kit's field family (src/design/KIT.md "Fields"): one filled surface, no outline, 12px radius; the label above
|
|
269
|
+
names the field and focus draws the ink ring. A transparent border keeps the edge in forced colours. Textareas
|
|
270
|
+
grow with their words (no resize grip); selects draw the kit's chevron. */
|
|
271
|
+
:where(input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], input:not([type]), textarea, select) {
|
|
272
|
+
width: 100%; margin: .35rem 0 0; padding: .5rem .75rem;
|
|
273
|
+
color: var(--foreground); background-color: var(--so-field); min-height: 2.25rem;
|
|
274
|
+
border: 1px solid transparent; border-radius: var(--so-radius-sheet); box-shadow: none;
|
|
275
|
+
transition: background-color .12s;
|
|
276
|
+
}
|
|
277
|
+
:where(textarea) { resize: none; field-sizing: content; min-height: 4.75rem; max-height: 22rem; overflow-y: auto; line-height: 1.5; }
|
|
278
|
+
:where(select:not([multiple])) {
|
|
279
|
+
appearance: none; -webkit-appearance: none; padding-right: 2.1rem; cursor: pointer;
|
|
280
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--so-muted) 50%), linear-gradient(135deg, var(--so-muted) 50%, transparent 50%);
|
|
281
|
+
background-position: calc(100% - 17px) 52%, calc(100% - 12px) 52%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
|
|
282
|
+
}
|
|
283
|
+
:where(input, textarea, select)::placeholder { color: var(--muted-foreground); opacity: 1; }
|
|
274
284
|
/* The box above is in :where() so a surface's own rules (the workspace's 44px phone targets) win; the type keeps
|
|
275
285
|
its old weight, so the phone's 16px rule still loses to it as before. */
|
|
276
286
|
input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select { font: 400 0.875rem/1.4 var(--font-sans); }
|
|
277
|
-
@media (hover: hover) and (pointer: fine) { input:
|
|
287
|
+
@media (hover: hover) and (pointer: fine) { :where(input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], input:not([type]), textarea, select):hover { background-color: color-mix(in srgb, var(--so-field) 94%, var(--so-ink)); } }
|
|
278
288
|
input[type=number] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
279
289
|
input[type=radio], input[type=checkbox] { accent-color: var(--ring); }
|
|
280
290
|
.permission-field { border: 0; padding: 0; margin: 1rem 0 0; min-width: 0; }
|
|
@@ -312,7 +322,8 @@
|
|
|
312
322
|
min-height: 8.5rem; max-height: 18rem; margin: 0; padding: .9rem 1rem; resize: vertical;
|
|
313
323
|
border: 0; background: transparent; box-shadow: none; font-size: 1.05rem; line-height: 1.55;
|
|
314
324
|
}
|
|
315
|
-
.task-prompt textarea:focus-visible { border: 0; box-shadow: none; }
|
|
325
|
+
.task-prompt textarea:focus-visible { border: 0; box-shadow: none; outline: none; }
|
|
326
|
+
.task-composer:focus-within { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
316
327
|
@media (hover: hover) and (pointer: fine) { .task-prompt textarea:hover { border: 0; box-shadow: none; } }
|
|
317
328
|
.task-repo { margin: .25rem .45rem .7rem; }
|
|
318
329
|
.task-composer-footer { display: flex; align-items: center; gap: .5rem; padding: .25rem; }
|
|
@@ -361,7 +372,7 @@
|
|
|
361
372
|
}
|
|
362
373
|
input[type=password] { font-family: var(--font-mono); }
|
|
363
374
|
input:focus-visible, textarea:focus-visible, select:focus-visible {
|
|
364
|
-
outline:
|
|
375
|
+
outline: 2px solid var(--so-ring); outline-offset: 1px; box-shadow: none;
|
|
365
376
|
}
|
|
366
377
|
button:focus-visible, a:focus-visible, summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
|
|
367
378
|
|
|
@@ -2198,7 +2209,8 @@
|
|
|
2198
2209
|
width: 100%; min-height: 3.5rem; max-height: 13rem; margin: 0; padding: .75rem .85rem;
|
|
2199
2210
|
resize: vertical; border: 0; background: transparent; box-shadow: none; font-size: 1rem;
|
|
2200
2211
|
}
|
|
2201
|
-
.composer textarea:focus-visible { border: 0; box-shadow: none; }
|
|
2212
|
+
.composer textarea:focus-visible { border: 0; box-shadow: none; outline: none; }
|
|
2213
|
+
.composer:focus-within { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
2202
2214
|
@media (hover: hover) and (pointer: fine) { .composer textarea:hover { border: 0; box-shadow: none; } }
|
|
2203
2215
|
.composer button {
|
|
2204
2216
|
flex: 0 0 2.75rem; width: 2.75rem; min-height: 2.75rem; padding: 0; border-radius: 999px;
|
package/dist/server/flows.js
CHANGED
|
@@ -11,7 +11,9 @@ import { LIMITS } from "../decision.js";
|
|
|
11
11
|
import { run as execRun } from "../exec.js";
|
|
12
12
|
import { setFlowSecret } from "../flow-actions.js";
|
|
13
13
|
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowDefinitionOf, moveCardInFlow } from "../flow-engine.js";
|
|
14
|
-
import {
|
|
14
|
+
import { galleryUseHtml, newFlowHtml } from "../flow-gallery-ui.js";
|
|
15
|
+
import { startersHtml } from "../flow-starters-ui.js";
|
|
16
|
+
import { starterOf, startersFor } from "../flow-starters.js";
|
|
15
17
|
import { galleryAdmitsAnyone, galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
|
|
16
18
|
import { flowInsights } from "../flow-insights.js";
|
|
17
19
|
import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people.js";
|
|
@@ -24,11 +26,11 @@ import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneName
|
|
|
24
26
|
import { revokeFlowBrowser, signFlowBrowser } from "../flow-browser.js";
|
|
25
27
|
import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
|
|
26
28
|
import { keyStatus } from "../keys.js";
|
|
27
|
-
import { kitPageHtml,
|
|
29
|
+
import { kitPageHtml, kitRowsHtml } from "../kits-ui.js";
|
|
28
30
|
import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
|
|
29
31
|
import { connectionsOf } from "../mcp-connect.js";
|
|
30
32
|
import { authorizedProject, projectName, } from "../project.js";
|
|
31
|
-
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm,
|
|
33
|
+
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeRowsHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
|
|
32
34
|
import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
|
|
33
35
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
34
36
|
import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
|
|
@@ -59,7 +61,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
59
61
|
const diagram = preview?.built.definition ?? galleryDiagram(template);
|
|
60
62
|
const page = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
|
|
61
63
|
csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
|
|
62
|
-
return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name,
|
|
64
|
+
return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, page, { chrome: chromeFor(repo, "flows") }));
|
|
63
65
|
}
|
|
64
66
|
// ---- path parameters and shared lookups ------------------------------------
|
|
65
67
|
/** One segment of the path the route's pattern admitted ("/flows/1/cards/2/move" → 2 is "1", 4 is "2"). */
|
|
@@ -76,15 +78,50 @@ export function createFlowsHandlers(runtime) {
|
|
|
76
78
|
};
|
|
77
79
|
const sendJson = (response, status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
78
80
|
// ---- flows: reads ----------------------------------------------------------
|
|
79
|
-
// Flows → New: the
|
|
81
|
+
// Flows → New: describe it to the lead, or start from a template (with the kits, recipes and starter flows as groups).
|
|
80
82
|
async function newFlowGallery(ctx) {
|
|
81
83
|
const { url, response, who, project } = ctx;
|
|
82
84
|
const projects = consoleProjects();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
+
const approver = who.via === "cookie" && who.role === "approver";
|
|
86
|
+
const canUse = approver && projects.length > 0;
|
|
87
|
+
// The chosen project, else the one being looked at, else the first: its tools are what each row says is connected.
|
|
85
88
|
const asked = url.searchParams.get("repo");
|
|
86
|
-
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
|
|
87
|
-
|
|
89
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : projects[0] ?? null;
|
|
90
|
+
// Describe what you want: the words go to the lead as a chat draft, which drafts the flow for the person to confirm.
|
|
91
|
+
const described = url.searchParams.get("describe");
|
|
92
|
+
let askError = null;
|
|
93
|
+
if (described !== null && canUse) {
|
|
94
|
+
const words = described.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").trim();
|
|
95
|
+
const named = projects.find(one => projectName(one) === url.searchParams.get("project")) ?? repo;
|
|
96
|
+
if (words !== "")
|
|
97
|
+
return redirect(response, `/chat?draft=${encodeURIComponent(`Make a flow${named === null ? "" : ` in ${projectName(named)}`}: ${words}`.slice(0, 4000))}`);
|
|
98
|
+
askError = "Describe what should happen first.";
|
|
99
|
+
}
|
|
100
|
+
// One-time work: the open project's recipes (recipe screens work in the open project).
|
|
101
|
+
let recipes;
|
|
102
|
+
try {
|
|
103
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
104
|
+
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
|
|
105
|
+
const d = importRecipe(String(row["document"]));
|
|
106
|
+
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
107
|
+
const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
|
|
108
|
+
const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
|
|
109
|
+
return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
|
|
110
|
+
});
|
|
111
|
+
recipes = recipeRowsHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, project === null ? null : projectName(project), revision, recent);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (!(error instanceof RecipeError))
|
|
115
|
+
throw error;
|
|
116
|
+
recipes = html `<p class="k-notice" role="alert">${error.message}</p>`;
|
|
117
|
+
}
|
|
118
|
+
const starters = repo === null ? html `` : startersHtml({ repo, projectName: projectName(repo), starters: startersFor(store, repo), canSwitch: approver && !store.isDemo(),
|
|
119
|
+
suggested: starterOf(url.searchParams.get("starter") ?? "")?.id ?? null });
|
|
120
|
+
const said = url.searchParams.get("said"), problem = url.searchParams.get("problem");
|
|
121
|
+
const page = newFlowHtml({ repo, projects: projects.map(path => ({ path, name: projectName(path) })), canUse, group: url.searchParams.get("group") ?? "all",
|
|
122
|
+
connections: repo === null ? null : connectionsOf(store, repo), ask: { value: described ?? "", error: askError }, kits: kitRowsHtml(store, repo), recipes, starters,
|
|
123
|
+
said: said === null ? null : said.slice(0, 400), problem: problem === null ? null : problem.slice(0, 400) });
|
|
124
|
+
return sendScreen(response, 200, screen("New flow", page, { chrome: chromeFor(repo ?? project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
|
|
88
125
|
}
|
|
89
126
|
async function galleryTemplatePage(ctx) {
|
|
90
127
|
const { url, who, response, project } = ctx;
|
|
@@ -107,7 +144,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
107
144
|
const { url, who, response, project } = ctx;
|
|
108
145
|
const projects = consoleProjects();
|
|
109
146
|
const flows = store.listFlows(projects);
|
|
110
|
-
return sendScreen(response, 200, screen("Flows",
|
|
147
|
+
return sendScreen(response, 200, screen("Flows", flowsListHtml(store, flows, projects, who.via === "cookie" && who.role === "approver", url.searchParams.get("problem"), url.searchParams.get("project"), clock()), { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
|
|
111
148
|
}
|
|
112
149
|
async function flowInsightsRead(ctx) {
|
|
113
150
|
const { url, response } = ctx;
|
|
@@ -157,10 +194,10 @@ export function createFlowsHandlers(runtime) {
|
|
|
157
194
|
}
|
|
158
195
|
// ---- starter kits: the gallery, and each kit's checklist in a project ------
|
|
159
196
|
async function kitsGallery(ctx) {
|
|
160
|
-
const { url,
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
return
|
|
197
|
+
const { url, response } = ctx;
|
|
198
|
+
// Starter kits live under New flow → With an agent now; a refusal sent here still shows there.
|
|
199
|
+
const problem = url.searchParams.get("problem");
|
|
200
|
+
return redirect(response, `/flows/new?group=kits${problem === null ? "" : `&problem=${encodeURIComponent(problem.slice(0, 400))}`}`);
|
|
164
201
|
}
|
|
165
202
|
async function kitPage(ctx) {
|
|
166
203
|
const { url, who, response } = ctx;
|
|
@@ -169,8 +206,8 @@ export function createFlowsHandlers(runtime) {
|
|
|
169
206
|
const kit = kitOf(pathPart(url, 2));
|
|
170
207
|
const repo = url.searchParams.get("repo") ?? (projects.length === 1 ? projects[0] : "");
|
|
171
208
|
if (kit === null || !projects.includes(repo))
|
|
172
|
-
return redirect(response, "/kits");
|
|
173
|
-
return sendScreen(response, 200, screen(kit.name,
|
|
209
|
+
return redirect(response, "/flows/new?group=kits");
|
|
210
|
+
return sendScreen(response, 200, screen(kit.name, kitPageHtml(store, kit, repo, options.configDir ?? null, canSetUp, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }, projectName), { chrome: chromeFor(repo, "flows") }));
|
|
174
211
|
}
|
|
175
212
|
// ---- v92 / D5: the lead's subagents — the list, and one page per subagent ----
|
|
176
213
|
async function subagentsPage(ctx) {
|
|
@@ -200,24 +237,9 @@ export function createFlowsHandlers(runtime) {
|
|
|
200
237
|
response.writeHead(200, { ...SAFETY, "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="${mate.handle}.md"` });
|
|
201
238
|
return void response.end(mate.soul);
|
|
202
239
|
}
|
|
240
|
+
/** Recipes are New flow → One-time work now: the old address lands there. */
|
|
203
241
|
async function recipeLibrary(ctx) {
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
207
|
-
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
|
|
208
|
-
const d = importRecipe(String(row["document"]));
|
|
209
|
-
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
210
|
-
const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
|
|
211
|
-
const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
|
|
212
|
-
return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
|
|
213
|
-
});
|
|
214
|
-
return sendScreen(response, 200, screen("Workflow recipes", recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, revision, recent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
if (error instanceof RecipeError)
|
|
218
|
-
return refuse(response, who, error.status, error.message, "/recipes");
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
242
|
+
return redirect(ctx.response, "/flows/new?group=recipes");
|
|
221
243
|
}
|
|
222
244
|
/** One recipe screen in the open project: the editor (start, new, edit, from a task), use, preview, export; any other address is no screen. */
|
|
223
245
|
const recipeScreen = (shown) => async (ctx) => {
|
|
@@ -537,7 +559,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
537
559
|
// What was previewed: the file and every value it resolved to (a default the first preview filled in included).
|
|
538
560
|
const previewedOf = (plan) => createHash("sha256").update(`${document}\0${JSON.stringify(plan?.values ?? given)}`).digest("hex").slice(0, 32);
|
|
539
561
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
540
|
-
const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`,
|
|
562
|
+
const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`, flowImportHtml({ plan, file, document, repo, csrf, values: given, previewed: previewedOf(plan), problem }), { chrome: chromeFor(repo, "flows") }));
|
|
541
563
|
let plan;
|
|
542
564
|
try {
|
|
543
565
|
plan = planFlowImport(store, repo, file, given, who.name);
|
|
@@ -614,8 +636,14 @@ export function createFlowsHandlers(runtime) {
|
|
|
614
636
|
}
|
|
615
637
|
if (action === "scripts") {
|
|
616
638
|
// The project's script library, reached from any of its flows.
|
|
617
|
-
if (body.get("approve") === "yes")
|
|
618
|
-
|
|
639
|
+
if (body.get("approve") === "yes") {
|
|
640
|
+
// The approval covers the version and content the panel showed (its version and digest ride back).
|
|
641
|
+
const version = Number(body.get("version")), digest = body.get("digest") ?? "";
|
|
642
|
+
const approved = Number.isSafeInteger(version) && version > 0 && digest !== "" ? store.approveFlowScript(flow.repo, body.get("name") ?? "", { version, digest }) : "changed";
|
|
643
|
+
return approved === "approved" ? settle("Approved. Zones that run it go on.")
|
|
644
|
+
: approved === "changed" ? answer(409, { ok: false, said: "This script changed since you read it. Read the new script, then approve it. Nothing was approved." })
|
|
645
|
+
: answer(404, { ok: false, said: "That script isn't waiting for approval." });
|
|
646
|
+
}
|
|
619
647
|
if (body.get("remove") === "yes")
|
|
620
648
|
return store.removeFlowScript(flow.repo, body.get("name") ?? "", who.name, now) ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
|
|
621
649
|
const saved = saveScript(store, flow.repo, { name: body.get("name"), about: body.get("about"), body: body.get("body"), timeoutMinutes: body.get("timeoutMinutes"), language: body.get("language"), file: body.get("file") }, who.name, now);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home ("/", UI revamp wave B): one sentence that answers "what needs me?", four counts read from the same work index
|
|
3
|
+
* the Tasks list filters (so a count and the list it opens always agree), what waits on this person grouped by what it
|
|
4
|
+
* asks, what is running, and the lead's message box. Reads only: every act keeps its own route and rules (an agent's
|
|
5
|
+
* question is answered through POST /d/<id>/answer, the same one-click act as its own page).
|
|
6
|
+
*/
|
|
7
|
+
import type { BrowserHomeView } from "../browser-workspace.js";
|
|
8
|
+
import type { Store } from "../store.js";
|
|
9
|
+
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
10
|
+
/** How many waiting rows Home lists; the rest open in Tasks. */
|
|
11
|
+
export declare const HOME_NEEDS_SHOWN = 30;
|
|
12
|
+
export type HomeFacts = {
|
|
13
|
+
store: Store;
|
|
14
|
+
now: Date;
|
|
15
|
+
access: WorkSummaryAccess;
|
|
16
|
+
evidenceRoot?: string;
|
|
17
|
+
/** The project in view (already admitted), or null for every project. */
|
|
18
|
+
project: string | null;
|
|
19
|
+
/** Every admitted project, for the scope links. */
|
|
20
|
+
projects: readonly {
|
|
21
|
+
name: string;
|
|
22
|
+
path: string;
|
|
23
|
+
}[];
|
|
24
|
+
/** Chat is allowed for this person here (the lead's message box). */
|
|
25
|
+
chat: boolean;
|
|
26
|
+
/** This person can answer an agent's question (an approver in a browser session). */
|
|
27
|
+
canAnswer: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
|
|
30
|
+
export declare function homeSentence(needs: number, perProject: readonly {
|
|
31
|
+
name: string;
|
|
32
|
+
needs: number;
|
|
33
|
+
}[], scope: string | null, any: boolean): string;
|
|
34
|
+
/** Home's view for one person and scope, from two reads of the work index (what waits, what runs). */
|
|
35
|
+
export declare function homeView(facts: HomeFacts): BrowserHomeView;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { browserWorkActionHref } from "../browser-crew.js";
|
|
2
|
+
import { ASK_LABEL } from "../needs-you.js";
|
|
3
|
+
import { projectName } from "../project.js";
|
|
4
|
+
import { workIndexPage } from "../work-index.js";
|
|
5
|
+
import { relativeAge } from "./chrome.js";
|
|
6
|
+
import { taskHref } from "./http.js";
|
|
7
|
+
const TONE = { attention: "needs", ready: "needs", live: "live", done: "done", problem: "failed", muted: "", neutral: "" };
|
|
8
|
+
const ASKS = ["decide", "review", "unblock"];
|
|
9
|
+
const ASK_WORD = { decide: "Decision", review: "Result", unblock: "Blocked" };
|
|
10
|
+
/** How many waiting rows Home lists; the rest open in Tasks. */
|
|
11
|
+
export const HOME_NEEDS_SHOWN = 30;
|
|
12
|
+
const HOME_RUNNING_SHOWN = 8;
|
|
13
|
+
/** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
|
|
14
|
+
export function homeSentence(needs, perProject, scope, any) {
|
|
15
|
+
if (!any)
|
|
16
|
+
return "No tasks yet.";
|
|
17
|
+
if (needs === 0)
|
|
18
|
+
return scope === null ? "Nothing needs you." : `Nothing needs you in ${scope}.`;
|
|
19
|
+
const verb = needs === 1 ? "needs" : "need";
|
|
20
|
+
if (scope !== null)
|
|
21
|
+
return `${needs} ${verb} you in ${scope}.`;
|
|
22
|
+
const waiting = perProject.filter(one => one.needs > 0);
|
|
23
|
+
return waiting.length === 1 ? `${needs} ${verb} you in ${waiting[0].name}.` : `${needs} ${verb} you across ${waiting.length} projects.`;
|
|
24
|
+
}
|
|
25
|
+
function rowOf(store, item, now, multi, canAnswer) {
|
|
26
|
+
const action = item.primaryAction;
|
|
27
|
+
const href = action?.code === "open-result" && action.target.runId != null
|
|
28
|
+
? `/review?result=${encodeURIComponent(action.target.taskId)}&run=${action.target.runId}` : browserWorkActionHref(item);
|
|
29
|
+
const decisionId = action?.target.decisionId ?? null;
|
|
30
|
+
const decision = decisionId === null ? null : store.getDecision(decisionId);
|
|
31
|
+
const open = decision !== null && decision.state === "open" && decision.supersededReason === null;
|
|
32
|
+
// An irreversible option is armed behind a confirmation on its own page; Home answers only what one click may.
|
|
33
|
+
const question = open && canAnswer && decision.options.every(option => option.reversible) ? {
|
|
34
|
+
id: decision.id, text: decision.question, recap: decision.recap,
|
|
35
|
+
options: decision.options.map(option => ({ id: option.id, label: option.label, consequence: option.consequence, recommended: option.id === decision.recommendation })),
|
|
36
|
+
} : null;
|
|
37
|
+
const waits = item.status.views.includes("needs-you");
|
|
38
|
+
// The specific ask, never the group's own words again: "Needs you" under Unblock reads "Blocked".
|
|
39
|
+
const word = waits ? (open ? "Question" : item.chip ?? (item.status.label === "Needs you" && item.ask !== null ? ASK_WORD[item.ask] : item.status.label)) : item.status.label;
|
|
40
|
+
const tone = waits ? "ask" : TONE[item.status.tone];
|
|
41
|
+
const line = waits
|
|
42
|
+
? (open ? decision.question : item.status.detail !== item.status.label ? item.status.detail : null)
|
|
43
|
+
: item.progress ?? (item.status.detail !== item.status.label ? item.status.detail : null);
|
|
44
|
+
return {
|
|
45
|
+
id: item.rootId, title: item.title, href: taskHref(item.rootId),
|
|
46
|
+
project: multi || item.repo === null ? (item.repo === null ? "Unplaced" : projectName(item.repo)) : null,
|
|
47
|
+
age: relativeAge(item.updatedAt, now), chip: { word, tone }, line,
|
|
48
|
+
action: href === null || action === null ? null : { label: action.label, href }, question,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Home's view for one person and scope, from two reads of the work index (what waits, what runs). */
|
|
52
|
+
export function homeView(facts) {
|
|
53
|
+
const { store, now, access, project } = facts;
|
|
54
|
+
const read = (view, limit) => workIndexPage(store, now, access, { view, limit, project, ...(facts.evidenceRoot === undefined ? {} : { root: facts.evidenceRoot }) });
|
|
55
|
+
const needs = read("needs-you", HOME_NEEDS_SHOWN);
|
|
56
|
+
const running = read("running", HOME_RUNNING_SHOWN);
|
|
57
|
+
const totals = needs.totals;
|
|
58
|
+
// Queued: the Tasks list's own Queued count (the same predicate its view=queued filter lists).
|
|
59
|
+
const queued = totals.queued;
|
|
60
|
+
// Every project is said by name (all=1): /work alone would narrow to the session's open project.
|
|
61
|
+
const scopeQuery = project === null ? "all=1&" : `project=${encodeURIComponent(project)}&`;
|
|
62
|
+
const counts = [
|
|
63
|
+
{ key: "needs", label: "Needs you", value: totals["needs-you"], href: `/work?${scopeQuery}view=needs-you` },
|
|
64
|
+
{ key: "running", label: "Running", value: totals.running, href: `/work?${scopeQuery}view=running` },
|
|
65
|
+
{ key: "queued", label: "Queued", value: queued, href: `/work?${scopeQuery}view=queued` },
|
|
66
|
+
{ key: "finished", label: "Finished", value: totals.completed, href: `/work?${scopeQuery}view=completed` },
|
|
67
|
+
];
|
|
68
|
+
const multi = project === null && new Set([...needs.items, ...running.items].map(one => one.repo ?? "")).size > 1;
|
|
69
|
+
const groups = [];
|
|
70
|
+
for (const ask of ASKS) {
|
|
71
|
+
const rows = needs.items.filter(one => one.ask === ask).map(one => rowOf(store, one, now, multi, facts.canAnswer));
|
|
72
|
+
if (rows.length > 0)
|
|
73
|
+
groups.push({ key: ask, label: ASK_LABEL[ask], rows });
|
|
74
|
+
}
|
|
75
|
+
// A waiting row whose ask the index couldn't name still waits on this person: it goes under Decide, never lost.
|
|
76
|
+
const unnamed = needs.items.filter(one => one.ask === null).map(one => rowOf(store, one, now, multi, facts.canAnswer));
|
|
77
|
+
if (unnamed.length > 0) {
|
|
78
|
+
const decide = groups.find(one => one.key === "decide");
|
|
79
|
+
if (decide)
|
|
80
|
+
decide.rows.push(...unnamed);
|
|
81
|
+
else
|
|
82
|
+
groups.unshift({ key: "decide", label: ASK_LABEL.decide, rows: unnamed });
|
|
83
|
+
}
|
|
84
|
+
const live = running.items.filter(one => !one.status.views.includes("needs-you")).map(one => rowOf(store, one, now, multi, facts.canAnswer));
|
|
85
|
+
if (live.length > 0)
|
|
86
|
+
groups.push({ key: "running", label: "Running", rows: live });
|
|
87
|
+
const perProject = needs.projects.map(one => ({ name: one.repo === null ? "Unplaced" : projectName(one.repo), needs: one.totals["needs-you"] }));
|
|
88
|
+
const any = totals.all > 0 || totals.completed > 0 || (totals.archived ?? 0) > 0;
|
|
89
|
+
const shown = groups.filter(one => one.key !== "running").reduce((sum, one) => sum + one.rows.length, 0);
|
|
90
|
+
return {
|
|
91
|
+
kind: "home",
|
|
92
|
+
say: homeSentence(totals["needs-you"], perProject, project === null ? null : projectName(project), any),
|
|
93
|
+
project,
|
|
94
|
+
scopes: facts.projects.length < 2 ? null : [
|
|
95
|
+
{ key: "", label: "All projects", href: "/", active: project === null },
|
|
96
|
+
...facts.projects.map(one => ({ key: one.path, label: one.name, href: `/?project=${encodeURIComponent(one.path)}`, active: one.path === project })),
|
|
97
|
+
],
|
|
98
|
+
counts, groups,
|
|
99
|
+
more: totals["needs-you"] > shown ? { count: totals["needs-you"] - shown, href: counts[0].href } : null,
|
|
100
|
+
newTask: "/tasks/new",
|
|
101
|
+
ask: facts.chat ? { project, placeholder: project === null ? "Ask the lead about your projects…" : `Ask the lead about ${projectName(project)}…` } : null,
|
|
102
|
+
empty: any ? null : { title: "No tasks yet.", text: "Describe a change in a sentence; an agent writes a plan for you to approve." },
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -304,3 +304,6 @@ export declare const reviewHref: (taskId: string, runId?: number | null) => stri
|
|
|
304
304
|
* has room and as a dedicated view with Back to chat when it does not. */
|
|
305
305
|
export declare const chatResultHref: (taskId: string, runId: number, tab?: ResultTab) => string;
|
|
306
306
|
export declare function form(request: IncomingMessage, cap?: number): Promise<URLSearchParams>;
|
|
307
|
+
/** A refused chat setting (POST /chat/config): back on the lead's settings it is a ?problem=, which the page's section
|
|
308
|
+
* shows in place with the edits kept; elsewhere it stays the chat's said line. */
|
|
309
|
+
export declare function chatRefusal(back: string, words: string): string;
|