toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- 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/assignment.js +4 -3
- 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 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- 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/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- 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.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- 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/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- 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 +249 -6
- 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-delete.js +7 -3
- 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-flow.js +35 -4
- 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 +21 -0
- package/dist/release.js +40 -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/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -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 +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -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 +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -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 +35 -190
- package/dist/server/render-pages.js +154 -563
- 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 +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- 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/settings-proofs.js +1 -0
- 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 +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -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/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- 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.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -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";
|
|
@@ -389,9 +389,11 @@ export function createChatHandlers(runtime) {
|
|
|
389
389
|
const id = proposalId, saved = store.getLeadProposal(id), savedAction = saved?.kind === 'action' ? sharedActionPayload(saved.payload) : null;
|
|
390
390
|
if (saved && savedAction && saved.state !== 'pending' && (sharedConversation !== null || store.getLeadThread(saved.thread)?.approver === principal.name) && principal.repos.includes(savedAction.repo) && store.accountCanAccess(principal.name, savedAction.repo)) {
|
|
391
391
|
const task = typeof saved.outcome?.['taskId'] === 'string' ? saved.outcome['taskId'] : typeof savedAction.request['task'] === 'string' ? savedAction.request['task'] : null;
|
|
392
|
-
|
|
392
|
+
// A flow action leads back to its flow (a page path of our own making, never the proposal's words).
|
|
393
|
+
const flowId = savedAction.operation.startsWith('flow_') && Number.isSafeInteger(savedAction.request['flow']) ? Number(savedAction.request['flow']) : null;
|
|
394
|
+
const destination = task ? taskHref(task) : flowId !== null ? `/flows/${flowId}` : `/settings/${savedAction.operation.startsWith('skill_') ? 'skills' : 'knowledge'}?repo=${encodeURIComponent(savedAction.repo)}`;
|
|
393
395
|
const said = typeof saved.outcome?.['said'] === 'string' ? saved.outcome['said'] : saved.state === 'dismissed' ? 'Action dismissed.' : saved.state === 'expired' ? 'This proposal expired. Ask for a fresh proposal.' : 'This action has no completed outcome yet.';
|
|
394
|
-
return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
|
|
396
|
+
return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : flowId !== null ? 'Open flow' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
|
|
395
397
|
}
|
|
396
398
|
const review = mintSharedActionReview(store, principal, id, evidenceRoot, clock()), action = review.payload;
|
|
397
399
|
const terms = action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`);
|
|
@@ -525,18 +527,18 @@ export function createChatHandlers(runtime) {
|
|
|
525
527
|
return refuse(response, who, 403, "the sandbox never pushes");
|
|
526
528
|
const password = body.get("token") ?? "";
|
|
527
529
|
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
528
|
-
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`);
|
|
529
531
|
}
|
|
530
532
|
const endpoint = body.get("endpoint") ?? "";
|
|
531
533
|
const p256dh = body.get("p256dh") ?? "";
|
|
532
534
|
const auth = body.get("auth") ?? "";
|
|
533
535
|
const checked = validatePushEndpoint(endpoint);
|
|
534
536
|
if (!checked.ok)
|
|
535
|
-
return redirect(response, `/settings?said=${encodeURIComponent(checked.problem)}`);
|
|
537
|
+
return redirect(response, `/settings/alerts?said=${encodeURIComponent(checked.problem)}#this-device`);
|
|
536
538
|
const p256dhBytes = Buffer.from(p256dh, "base64url");
|
|
537
539
|
const authBytes = Buffer.from(auth, "base64url");
|
|
538
540
|
if (p256dhBytes.length !== 65 || p256dhBytes[0] !== 4 || authBytes.length !== 16) {
|
|
539
|
-
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`);
|
|
540
542
|
}
|
|
541
543
|
const keys = loadOrCreateVapidKeys(dirname(options.telegramTokenFile ?? "."));
|
|
542
544
|
const generation = store.approverGeneration(who.name) ?? 1;
|
|
@@ -550,9 +552,9 @@ export function createChatHandlers(runtime) {
|
|
|
550
552
|
vapidFingerprint: keys.fingerprint,
|
|
551
553
|
}, clock());
|
|
552
554
|
if (!enrolled.ok) {
|
|
553
|
-
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`);
|
|
554
556
|
}
|
|
555
|
-
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`);
|
|
556
558
|
}
|
|
557
559
|
async function pushRemove(ctx) {
|
|
558
560
|
const { who, response, posted } = ctx;
|
|
@@ -562,9 +564,9 @@ export function createChatHandlers(runtime) {
|
|
|
562
564
|
const id = Number(body.get("id") ?? "");
|
|
563
565
|
const mine = store.listPushSubscriptions(who.name).find(one => one.id === id && one.retiredAt === null);
|
|
564
566
|
if (mine === undefined)
|
|
565
|
-
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`);
|
|
566
568
|
store.retirePushSubscription(mine.id, `removed by ${who.name}`, clock());
|
|
567
|
-
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`);
|
|
568
570
|
}
|
|
569
571
|
async function phoneDismiss(ctx) {
|
|
570
572
|
const { who, response, now, posted } = ctx;
|
|
@@ -585,7 +587,7 @@ export function createChatHandlers(runtime) {
|
|
|
585
587
|
const back = body.get("return") === "/settings/lead" ? "/settings/lead" : safeChatReturn(body.get("return"));
|
|
586
588
|
const password = body.get("token") ?? "";
|
|
587
589
|
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
588
|
-
return redirect(response,
|
|
590
|
+
return redirect(response, chatRefusal(back, "configuring chat spend takes your password, typed again"));
|
|
589
591
|
}
|
|
590
592
|
if (body.get("off") === "1") {
|
|
591
593
|
store.clearChatConfig();
|
|
@@ -602,12 +604,12 @@ export function createChatHandlers(runtime) {
|
|
|
602
604
|
const weekly = Number(weeklyText);
|
|
603
605
|
const daily = (body.get("daily-turns") ?? "").trim() === "" ? 50 : Number(body.get("daily-turns"));
|
|
604
606
|
if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription") {
|
|
605
|
-
return redirect(response,
|
|
607
|
+
return redirect(response, chatRefusal(back, "pick a chat provider"));
|
|
606
608
|
}
|
|
607
609
|
const subscription = isSubscriptionChatProvider(provider);
|
|
608
610
|
const model = requestedModel === "" && subscription ? "default" : requestedModel;
|
|
609
611
|
if (!validModelId(model)) {
|
|
610
|
-
return redirect(response,
|
|
612
|
+
return redirect(response, chatRefusal(back, "the model id must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes"));
|
|
611
613
|
}
|
|
612
614
|
// The key, when pasted, is stored FIRST (0600 file, Telegram-token
|
|
613
615
|
// precedent) so the catalog fetch below can already use it. It never
|
|
@@ -615,10 +617,10 @@ export function createChatHandlers(runtime) {
|
|
|
615
617
|
const pastedKey = (body.get("key") ?? "").trim();
|
|
616
618
|
if (pastedKey !== "") {
|
|
617
619
|
if (subscription)
|
|
618
|
-
return redirect(response,
|
|
620
|
+
return redirect(response, chatRefusal(back, "subscription chat uses the CLI's cached login — do not paste an API key"));
|
|
619
621
|
const stored = storeChatKey(provider, pastedKey);
|
|
620
622
|
if (!stored.ok)
|
|
621
|
-
return redirect(response,
|
|
623
|
+
return redirect(response, chatRefusal(back, stored.message));
|
|
622
624
|
}
|
|
623
625
|
// The pin: anthropic models come from the compiled table; openrouter
|
|
624
626
|
// models come from OpenRouter's OWN catalog, priced by the authority
|
|
@@ -633,13 +635,13 @@ export function createChatHandlers(runtime) {
|
|
|
633
635
|
pin = hit.price;
|
|
634
636
|
}
|
|
635
637
|
if (!subscription && pin === null) {
|
|
636
|
-
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"));
|
|
637
639
|
}
|
|
638
640
|
if (!subscription && (!Number.isFinite(weekly) || weekly <= 0)) {
|
|
639
|
-
return redirect(response,
|
|
641
|
+
return redirect(response, chatRefusal(back, "the weekly ceiling is a positive dollar amount — chat without one is unbounded, not configured"));
|
|
640
642
|
}
|
|
641
643
|
if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000) {
|
|
642
|
-
return redirect(response,
|
|
644
|
+
return redirect(response, chatRefusal(back, "daily turns is a whole number between 1 and 1000"));
|
|
643
645
|
}
|
|
644
646
|
store.setChatConfig({
|
|
645
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 {
|
|
@@ -2742,3 +2742,32 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
|
|
|
2742
2742
|
|
|
2743
2743
|
.result-feedback-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
2744
2744
|
.result-feedback-actions button { min-height: 44px; white-space: nowrap; }
|
|
2745
|
+
|
|
2746
|
+
/* Put away old tasks (docs/plans/task-tidy.md): grouped checklist on the kit's column. */
|
|
2747
|
+
.put-away { max-width: var(--so-settings-width, 760px); }
|
|
2748
|
+
.put-away h1 { margin: 0 0 4px; }
|
|
2749
|
+
.put-away-days { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 16px; }
|
|
2750
|
+
.put-away-days label { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
|
|
2751
|
+
.put-away-days input[type=number] { width: 5rem; margin: 0; }
|
|
2752
|
+
.put-away-days button { width: auto; }
|
|
2753
|
+
.put-away-group { margin: 0 0 20px; padding: 0; border: 0; }
|
|
2754
|
+
.put-away-group legend { display: flex; align-items: center; gap: 8px; padding: 0; margin: 0 0 6px; font-weight: 600; font-size: 15px; }
|
|
2755
|
+
.put-away-group legend .count { font-family: var(--so-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; color: var(--so-muted); }
|
|
2756
|
+
.put-away-group ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--so-line); }
|
|
2757
|
+
.put-away-group li { border-bottom: 1px solid var(--so-line); }
|
|
2758
|
+
.put-away-item { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: start; margin: 0; padding: 10px 2px; cursor: pointer; }
|
|
2759
|
+
.put-away-item input { margin: 3px 0 0; width: auto; min-height: 0; align-self: start; }
|
|
2760
|
+
.put-away-title { font-size: 13.5px; font-weight: 500; overflow-wrap: anywhere; }
|
|
2761
|
+
.put-away-why { grid-column: 2; font-size: 12.5px; color: var(--so-muted); }
|
|
2762
|
+
.put-away-id { grid-column: 2; font-family: var(--so-mono, ui-monospace, monospace); font-size: 11.5px; color: var(--so-muted); overflow-wrap: anywhere; }
|
|
2763
|
+
.put-away-cli { margin: 12px 0 0; font-size: 13px; color: var(--so-muted); overflow-wrap: anywhere; }
|
|
2764
|
+
.put-away-cli code { font-size: 12px; }
|
|
2765
|
+
.task-archived { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--so-raised); }
|
|
2766
|
+
.task-archived p { margin: 0; font-size: 13.5px; }
|
|
2767
|
+
.task-archived form { margin: 0; }
|
|
2768
|
+
.task-archived button { width: auto; }
|
|
2769
|
+
.work-row-status form.inline { margin: 0; }
|
|
2770
|
+
@media (max-width: 900px) {
|
|
2771
|
+
.put-away-item { min-height: 44px; }
|
|
2772
|
+
.put-away-days button, .task-archived button, .work-row-status form.inline button { min-height: 44px; }
|
|
2773
|
+
}
|
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";
|
|
@@ -21,13 +23,14 @@ import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, pl
|
|
|
21
23
|
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
|
|
22
24
|
import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
|
|
23
25
|
import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
|
|
26
|
+
import { revokeFlowBrowser, signFlowBrowser } from "../flow-browser.js";
|
|
24
27
|
import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
|
|
25
28
|
import { keyStatus } from "../keys.js";
|
|
26
|
-
import { kitPageHtml,
|
|
29
|
+
import { kitPageHtml, kitRowsHtml } from "../kits-ui.js";
|
|
27
30
|
import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
|
|
28
31
|
import { connectionsOf } from "../mcp-connect.js";
|
|
29
32
|
import { authorizedProject, projectName, } from "../project.js";
|
|
30
|
-
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm,
|
|
33
|
+
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeRowsHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
|
|
31
34
|
import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
|
|
32
35
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
33
36
|
import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
|
|
@@ -58,7 +61,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
58
61
|
const diagram = preview?.built.definition ?? galleryDiagram(template);
|
|
59
62
|
const page = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
|
|
60
63
|
csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
|
|
61
|
-
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") }));
|
|
62
65
|
}
|
|
63
66
|
// ---- path parameters and shared lookups ------------------------------------
|
|
64
67
|
/** One segment of the path the route's pattern admitted ("/flows/1/cards/2/move" → 2 is "1", 4 is "2"). */
|
|
@@ -75,15 +78,50 @@ export function createFlowsHandlers(runtime) {
|
|
|
75
78
|
};
|
|
76
79
|
const sendJson = (response, status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
77
80
|
// ---- flows: reads ----------------------------------------------------------
|
|
78
|
-
// 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).
|
|
79
82
|
async function newFlowGallery(ctx) {
|
|
80
83
|
const { url, response, who, project } = ctx;
|
|
81
84
|
const projects = consoleProjects();
|
|
82
|
-
const
|
|
83
|
-
|
|
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.
|
|
84
88
|
const asked = url.searchParams.get("repo");
|
|
85
|
-
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
|
|
86
|
-
|
|
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 } }));
|
|
87
125
|
}
|
|
88
126
|
async function galleryTemplatePage(ctx) {
|
|
89
127
|
const { url, who, response, project } = ctx;
|
|
@@ -106,7 +144,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
106
144
|
const { url, who, response, project } = ctx;
|
|
107
145
|
const projects = consoleProjects();
|
|
108
146
|
const flows = store.listFlows(projects);
|
|
109
|
-
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 } }));
|
|
110
148
|
}
|
|
111
149
|
async function flowInsightsRead(ctx) {
|
|
112
150
|
const { url, response } = ctx;
|
|
@@ -156,10 +194,10 @@ export function createFlowsHandlers(runtime) {
|
|
|
156
194
|
}
|
|
157
195
|
// ---- starter kits: the gallery, and each kit's checklist in a project ------
|
|
158
196
|
async function kitsGallery(ctx) {
|
|
159
|
-
const { url,
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
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))}`}`);
|
|
163
201
|
}
|
|
164
202
|
async function kitPage(ctx) {
|
|
165
203
|
const { url, who, response } = ctx;
|
|
@@ -168,8 +206,8 @@ export function createFlowsHandlers(runtime) {
|
|
|
168
206
|
const kit = kitOf(pathPart(url, 2));
|
|
169
207
|
const repo = url.searchParams.get("repo") ?? (projects.length === 1 ? projects[0] : "");
|
|
170
208
|
if (kit === null || !projects.includes(repo))
|
|
171
|
-
return redirect(response, "/kits");
|
|
172
|
-
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") }));
|
|
173
211
|
}
|
|
174
212
|
// ---- v92 / D5: the lead's subagents — the list, and one page per subagent ----
|
|
175
213
|
async function subagentsPage(ctx) {
|
|
@@ -199,24 +237,9 @@ export function createFlowsHandlers(runtime) {
|
|
|
199
237
|
response.writeHead(200, { ...SAFETY, "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="${mate.handle}.md"` });
|
|
200
238
|
return void response.end(mate.soul);
|
|
201
239
|
}
|
|
240
|
+
/** Recipes are New flow → One-time work now: the old address lands there. */
|
|
202
241
|
async function recipeLibrary(ctx) {
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
206
|
-
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 => {
|
|
207
|
-
const d = importRecipe(String(row["document"]));
|
|
208
|
-
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
209
|
-
const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
|
|
210
|
-
const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
|
|
211
|
-
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") };
|
|
212
|
-
});
|
|
213
|
-
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() } }));
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
if (error instanceof RecipeError)
|
|
217
|
-
return refuse(response, who, error.status, error.message, "/recipes");
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
242
|
+
return redirect(ctx.response, "/flows/new?group=recipes");
|
|
220
243
|
}
|
|
221
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. */
|
|
222
245
|
const recipeScreen = (shown) => async (ctx) => {
|
|
@@ -536,7 +559,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
536
559
|
// What was previewed: the file and every value it resolved to (a default the first preview filled in included).
|
|
537
560
|
const previewedOf = (plan) => createHash("sha256").update(`${document}\0${JSON.stringify(plan?.values ?? given)}`).digest("hex").slice(0, 32);
|
|
538
561
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
539
|
-
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") }));
|
|
540
563
|
let plan;
|
|
541
564
|
try {
|
|
542
565
|
plan = planFlowImport(store, repo, file, given, who.name);
|
|
@@ -613,8 +636,14 @@ export function createFlowsHandlers(runtime) {
|
|
|
613
636
|
}
|
|
614
637
|
if (action === "scripts") {
|
|
615
638
|
// The project's script library, reached from any of its flows.
|
|
616
|
-
if (body.get("approve") === "yes")
|
|
617
|
-
|
|
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
|
+
}
|
|
618
647
|
if (body.get("remove") === "yes")
|
|
619
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." });
|
|
620
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);
|
|
@@ -656,6 +685,18 @@ export function createFlowsHandlers(runtime) {
|
|
|
656
685
|
if (action === "unattended-off") {
|
|
657
686
|
return revokeUnattended(store, flow.id, who.name, now) ? settle("It no longer runs on its own. New cards wait for you.") : answer(409, { ok: false, said: "It wasn't running on its own." });
|
|
658
687
|
}
|
|
688
|
+
if (action === "browser") {
|
|
689
|
+
// A person's own step-up (a lead's or remote run's token never gets here: route table), then signFlowBrowser
|
|
690
|
+
// re-reads the flow, its zones and scripts in its one write and refuses unless they are the digest the panel showed.
|
|
691
|
+
const proved = authenticateApprover(who, body.get("password") ?? "", flow.repo);
|
|
692
|
+
if (!proved.ok)
|
|
693
|
+
return answer(403, { ok: false, said: "That password isn't right. Nothing was approved." });
|
|
694
|
+
const signed = signFlowBrowser(store, { flow: flow.id, signer: who.name, proof: proved.method, sawDigest: body.get("digest") ?? "", console: { generation: who.via === "cookie" ? who.session.generation : -1 } }, now);
|
|
695
|
+
return signed.ok ? settle(`Browser allowed. ${signed.grant.zones.length === 1 ? "Its script" : "Its scripts"} can start Chromium now.`) : answer(409, { ok: false, said: signed.message });
|
|
696
|
+
}
|
|
697
|
+
if (action === "browser-off") {
|
|
698
|
+
return revokeFlowBrowser(store, flow.id, who.name, now) ? settle("Browser turned off. Its browser zones wait until someone allows it again.") : answer(409, { ok: false, said: "Browser use wasn't allowed for this flow." });
|
|
699
|
+
}
|
|
659
700
|
if (action === "save") {
|
|
660
701
|
let saved, drawn;
|
|
661
702
|
try {
|
|
@@ -975,6 +1016,8 @@ export function createFlowsHandlers(runtime) {
|
|
|
975
1016
|
"flow.secrets": flowAct("secrets"),
|
|
976
1017
|
"flow.unattended": flowAct("unattended"),
|
|
977
1018
|
"flow.unattended-off": flowAct("unattended-off"),
|
|
1019
|
+
"flow.browser": flowAct("browser"),
|
|
1020
|
+
"flow.browser-off": flowAct("browser-off"),
|
|
978
1021
|
"flow.triggers": flowAct("triggers"),
|
|
979
1022
|
"flow.card.move": cardAct("move"),
|
|
980
1023
|
"flow.card.decide": cardAct("decide"),
|
|
@@ -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
|
+
}
|