toolroll 0.9.100 → 0.9.102
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 +15 -0
- package/dist/accent-colors.js +1 -1
- package/dist/activity-line.d.ts +1 -1
- package/dist/activity-line.js +1 -1
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +40 -40
- package/dist/browser-workspace.d.ts +78 -15
- package/dist/browser-workspace.js +5 -3
- package/dist/design/kit.css +94 -2
- package/dist/design/kit.d.ts +12 -4
- package/dist/design/kit.js +8 -2
- package/dist/design/tokens.css +5 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/first-run.d.ts +10 -6
- package/dist/first-run.js +19 -18
- package/dist/guides.js +1 -1
- package/dist/operate.js +2 -0
- package/dist/page-work-contract.d.ts +21 -0
- package/dist/page-work-contract.js +5 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/run-efficiency.d.ts +3 -2
- package/dist/run-efficiency.js +4 -3
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +8 -1
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +11 -2
- package/dist/server/chrome.js +10 -1
- package/dist/server/console.css +65 -21
- package/dist/server/home.d.ts +27 -14
- package/dist/server/home.js +78 -63
- package/dist/server/http.d.ts +6 -0
- package/dist/server/http.js +8 -0
- package/dist/server/in-place.d.ts +15 -0
- package/dist/server/in-place.js +32 -0
- package/dist/server/kit.d.ts +36 -3
- package/dist/server/kit.js +33 -4
- package/dist/server/pages.js +23 -2
- package/dist/server/render-chat.d.ts +2 -0
- package/dist/server/render-chat.js +1 -0
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-results.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +245 -65
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +11 -4
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/settings-groups-ui.js +1 -1
- package/dist/store.js +14 -3
- package/dist/task-archive.d.ts +3 -0
- package/dist/task-archive.js +5 -0
- package/dist/task-status.d.ts +2 -2
- package/dist/task-status.js +3 -3
- package/dist/tui.js +1 -1
- package/dist/work-index.d.ts +14 -1
- package/dist/work-index.js +24 -8
- package/dist/workspace-ui.d.ts +69 -0
- package/dist/workspace-ui.js +103 -0
- package/package.json +1 -1
package/dist/server/home.js
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
import { browserWorkActionHref } from "../browser-crew.js";
|
|
2
|
-
import {
|
|
2
|
+
import { NO_REASON_RECORDED } from "../needs-you.js";
|
|
3
3
|
import { projectName } from "../project.js";
|
|
4
|
-
import { workIndexPage } from "../work-index.js";
|
|
5
|
-
import { relativeAge } from "./chrome.js";
|
|
4
|
+
import { WORK_ASK_GROUPS, workIndexPage } from "../work-index.js";
|
|
5
|
+
import { oneLineOf, relativeAge } from "./chrome.js";
|
|
6
6
|
import { taskHref } from "./http.js";
|
|
7
|
-
|
|
8
|
-
const ASK_WORD = { decide: "Decision", review: "Result", unblock: "Blocked" };
|
|
9
|
-
/** How many waiting rows Home lists; the rest open in Tasks. */
|
|
7
|
+
/** How many waiting rows Home lists; each group says how many more open in Tasks. */
|
|
10
8
|
export const HOME_NEEDS_SHOWN = 30;
|
|
11
9
|
const HOME_RUNNING_SHOWN = 8;
|
|
12
|
-
/** How many rows finished
|
|
13
|
-
const HOME_DONE_SHOWN = 8;
|
|
14
|
-
/**
|
|
15
|
-
|
|
10
|
+
/** How many rows finished today Home lists; its figure counts them all. */
|
|
11
|
+
export const HOME_DONE_SHOWN = 8;
|
|
12
|
+
/** About one line of the lead's last message. */
|
|
13
|
+
const LEAD_LINE_LIMIT = 160;
|
|
14
|
+
/** Each group's heading, in the order Home shows them. */
|
|
15
|
+
export const HOME_ASK_LABEL = {
|
|
16
|
+
approve: "Approve a plan", answer: "Answer a question", wrong: "Something went wrong", review: "Review results", other: "Needs you",
|
|
17
|
+
};
|
|
18
|
+
/** "19 tasks need you": the heading, said once, from the same total as the rail's count. */
|
|
19
|
+
export function homeSentence(needs, any) {
|
|
16
20
|
if (!any)
|
|
17
|
-
return "No tasks yet
|
|
21
|
+
return "No tasks yet";
|
|
18
22
|
if (needs === 0)
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
if (scope !== null)
|
|
22
|
-
return `${needs} ${verb} you in ${scope}.`;
|
|
23
|
-
const waiting = perProject.filter(one => one.needs > 0);
|
|
24
|
-
return waiting.length === 1 ? `${needs} ${verb} you in ${waiting[0].name}.` : `${needs} ${verb} you across ${waiting.length} projects.`;
|
|
23
|
+
return "Nothing needs you";
|
|
24
|
+
return needs === 1 ? "1 task needs you" : `${needs} tasks need you`;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
/** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
|
|
27
|
+
export function leadLineOf(text) {
|
|
28
|
+
const plain = text
|
|
29
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
30
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
31
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
32
|
+
.replace(/^\s{0,3}(?:#{1,6}\s+|>\s?|[-*+]\s+|\d+[.)]\s+)/gm, "")
|
|
33
|
+
.replace(/(\*\*|__|~~)(.*?)\1/g, "$2")
|
|
34
|
+
.replace(/(^|[\s(])[*_]([^*_\n]+)[*_](?=[\s).,!?:;]|$)/g, "$1$2");
|
|
35
|
+
return oneLineOf(plain, LEAD_LINE_LIMIT);
|
|
36
|
+
}
|
|
37
|
+
/** Midnight today on this computer's clock: "finished today" starts here. */
|
|
38
|
+
export function startOfToday(now) {
|
|
39
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
40
|
+
}
|
|
41
|
+
function rowOf(store, item, now, multi, canAnswer, closedDoor, state) {
|
|
27
42
|
const action = item.primaryAction;
|
|
28
43
|
const href = action?.code === "open-result" && action.target.runId != null
|
|
29
44
|
? `/review?result=${encodeURIComponent(action.target.taskId)}&run=${action.target.runId}` : browserWorkActionHref(item);
|
|
@@ -31,87 +46,87 @@ function rowOf(store, item, now, multi, canAnswer, closedDoor) {
|
|
|
31
46
|
const decision = decisionId === null ? null : store.getDecision(decisionId);
|
|
32
47
|
const open = decision !== null && decision.state === "open" && decision.supersededReason === null;
|
|
33
48
|
// An irreversible option is armed behind a confirmation on its own page; Home answers only what one click may.
|
|
34
|
-
const question = open && canAnswer && decision.options.every(option => option.reversible) ? {
|
|
49
|
+
const question = state === "needs" && open && canAnswer && decision.options.every(option => option.reversible) ? {
|
|
35
50
|
id: decision.id, text: decision.question, recap: decision.recap,
|
|
36
51
|
options: decision.options.map(option => ({ id: option.id, label: option.label, consequence: option.consequence, recommended: option.id === decision.recommendation })),
|
|
37
52
|
} : null;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: item.progress ?? (item.status.detail !== item.status.label ? item.status.detail : null);
|
|
53
|
+
const ask = state === "needs" ? item.askGroup ?? "other" : null;
|
|
54
|
+
const said = item.status.detail !== item.status.label ? item.status.detail : null;
|
|
55
|
+
const line = state === "needs" && open ? decision.question : item.progress ?? said;
|
|
56
|
+
// Only a row where something went wrong says why, in one short line.
|
|
57
|
+
// With no reason on record, the row says what happened (its status) instead of a sentence about the missing reason.
|
|
58
|
+
const reason = ask === "wrong" ? oneLineOf((said !== null && said !== NO_REASON_RECORDED ? said : item.status.label).replace(/[.\s]+$/, ""), 120) : null;
|
|
45
59
|
// A plan that can't take a yes never offers one: the reason, and the one act that opens it, as the task page says.
|
|
46
|
-
const door = action?.code === "approve-scope" ? closedDoor(action.target.taskId) : null;
|
|
60
|
+
const door = state === "needs" && action?.code === "approve-scope" ? closedDoor(action.target.taskId) : null;
|
|
47
61
|
return {
|
|
48
|
-
id: item.rootId, title: item.title, href: taskHref(item.rootId),
|
|
62
|
+
id: item.rootId, title: item.title, href: taskHref(item.rootId), state, ask, reason,
|
|
49
63
|
project: multi || item.repo === null ? (item.repo === null ? "Unplaced" : projectName(item.repo)) : null,
|
|
50
|
-
age: relativeAge(item.updatedAt, now),
|
|
64
|
+
age: relativeAge(item.updatedAt, now), line: door === null ? line : door.title,
|
|
51
65
|
action: door !== null ? { label: door.road === "agents" ? "Change the agents" : "Edit the plan", href: `${taskHref(action.target.taskId)}#${door.road === "agents" ? "agents" : "scope"}` }
|
|
52
66
|
: href === null || action === null ? null : { label: action.label, href }, question,
|
|
53
67
|
};
|
|
54
68
|
}
|
|
55
|
-
/** Home's view for one person and scope, from four reads of the work index
|
|
56
|
-
*
|
|
69
|
+
/** Home's view for one person and scope, from four reads of the work index: what waits on you (with exact per-ask
|
|
70
|
+
* totals), what runs, what waits its turn, and what was completed today (filtered before its limit). */
|
|
57
71
|
export function homeView(facts) {
|
|
58
72
|
const { store, now, access, project } = facts;
|
|
59
|
-
const read = (view, limit) => workIndexPage(store, now, access, { view, limit, project, ...(
|
|
73
|
+
const read = (view, limit, completedSince) => workIndexPage(store, now, access, { view, limit, project, ...(completedSince === undefined ? {} : { completedSince }),
|
|
74
|
+
...(facts.evidenceRoot === undefined ? {} : { root: facts.evidenceRoot }) });
|
|
60
75
|
const needs = read("needs-you", HOME_NEEDS_SHOWN);
|
|
61
76
|
const running = read("running", HOME_RUNNING_SHOWN);
|
|
62
77
|
// Queued: what waits its turn, or what Toolroll or another task moves on, with no person asked.
|
|
63
78
|
const waiting = read("queued", HOME_RUNNING_SHOWN);
|
|
64
|
-
// Done: the Completed list
|
|
65
|
-
//
|
|
66
|
-
const
|
|
79
|
+
// Done today: the Completed list from midnight on, filtered in the read itself, so the figure and the rows are
|
|
80
|
+
// what finished today however many there are.
|
|
81
|
+
const today = read("completed", HOME_DONE_SHOWN, startOfToday(now).toISOString());
|
|
67
82
|
const totals = needs.totals;
|
|
68
|
-
// Queued: the Tasks list's own Queued count (the same predicate its view=queued filter lists).
|
|
69
|
-
const queued = totals.queued;
|
|
70
83
|
// Every project is said by name (scope=all): /tasks alone would narrow to the session's open project.
|
|
71
84
|
const scopeQuery = project === null ? "scope=all&" : `project=${encodeURIComponent(project)}&`;
|
|
72
|
-
// Each figure keeps the
|
|
85
|
+
// Each figure keeps the list it opens.
|
|
73
86
|
const counts = [
|
|
74
|
-
{ key: "needs", label:
|
|
87
|
+
{ key: "needs", label: "Needs you", value: totals["needs-you"], href: `/tasks?${scopeQuery}view=needs-you` },
|
|
75
88
|
{ key: "running", label: "Running", value: totals.running, href: `/tasks?${scopeQuery}view=running` },
|
|
76
|
-
{ key: "queued", label: "Queued", value: queued, href: `/tasks?${scopeQuery}view=queued` },
|
|
77
|
-
{ key: "finished", label:
|
|
89
|
+
{ key: "queued", label: "Queued", value: totals.queued, href: `/tasks?${scopeQuery}view=queued` },
|
|
90
|
+
{ key: "finished", label: "Finished", value: totals.completed, href: `/tasks?${scopeQuery}view=completed` },
|
|
78
91
|
];
|
|
79
|
-
// One row per family, in the first
|
|
92
|
+
// One row per family, in the first list that claims it.
|
|
80
93
|
const seen = new Set();
|
|
81
|
-
const take = (items,
|
|
82
|
-
if (
|
|
94
|
+
const take = (items, view) => items.filter(one => {
|
|
95
|
+
if (!one.status.views.includes(view) || seen.has(one.rootId))
|
|
83
96
|
return false;
|
|
84
97
|
seen.add(one.rootId);
|
|
85
98
|
return true;
|
|
86
99
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const groups =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
const perProject = needs.projects.map(one => ({ name: one.repo === null ? "Unplaced" : projectName(one.repo), needs: one.totals["needs-you"] }));
|
|
100
|
+
const yours = take(needs.items, "needs-you");
|
|
101
|
+
const live = take(running.items, "running");
|
|
102
|
+
const queued = take(waiting.items, "queued");
|
|
103
|
+
const done = take(today.items, "completed");
|
|
104
|
+
const multi = project === null && new Set([...yours, ...live, ...queued, ...done].map(one => one.repo ?? "")).size > 1;
|
|
105
|
+
const row = (item, state) => rowOf(store, item, now, multi, facts.canAnswer, facts.closedDoor, state);
|
|
106
|
+
const rows = yours.map(one => row(one, "needs"));
|
|
107
|
+
// Each group's count is the work index's exact total for that ask, over every row in view, not only those listed.
|
|
108
|
+
const groups = WORK_ASK_GROUPS.flatMap(key => {
|
|
109
|
+
const count = needs.asks[key];
|
|
110
|
+
const listed = rows.filter(one => one.ask === key);
|
|
111
|
+
return count === 0 && listed.length === 0 ? [] : [{ key, label: HOME_ASK_LABEL[key], count: Math.max(count, listed.length), rows: listed }];
|
|
112
|
+
});
|
|
101
113
|
const any = totals.all > 0 || totals.completed > 0 || (totals.archived ?? 0) > 0;
|
|
102
|
-
const
|
|
114
|
+
const lead = facts.chat ? facts.leadMessage ?? null : null;
|
|
115
|
+
const leadText = lead === null ? "" : leadLineOf(lead.text);
|
|
103
116
|
return {
|
|
104
117
|
kind: "home",
|
|
105
|
-
say: homeSentence(totals["needs-you"],
|
|
118
|
+
say: homeSentence(totals["needs-you"], any),
|
|
106
119
|
project,
|
|
107
120
|
scopes: facts.projects.length < 2 ? null : [
|
|
108
121
|
{ key: "", label: "All projects", href: "/", active: project === null },
|
|
109
122
|
...facts.projects.map(one => ({ key: one.path, label: one.name, href: `/?project=${encodeURIComponent(one.path)}`, active: one.path === project })),
|
|
110
123
|
],
|
|
111
124
|
counts, groups,
|
|
112
|
-
|
|
125
|
+
working: [...live.map(one => row(one, "running")), ...queued.map(one => row(one, "queued"))],
|
|
126
|
+
finishedToday: { count: Math.max(today.completedSince, done.length), rows: done.map(one => row(one, "done")), href: counts[3].href },
|
|
113
127
|
newTask: "/tasks/new",
|
|
114
|
-
ask: facts.chat ? { project, placeholder:
|
|
115
|
-
|
|
128
|
+
ask: facts.chat ? { project, placeholder: "Message the lead" } : null,
|
|
129
|
+
lastLead: lead === null || leadText === "" ? null : { text: leadText, age: relativeAge(lead.at, now), href: "/chat" },
|
|
130
|
+
empty: any ? null : { title: "No tasks yet", text: "Describe a change in a sentence; an agent writes a plan for you to approve." },
|
|
116
131
|
};
|
|
117
132
|
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -329,3 +329,9 @@ export declare function passwordSetNext(raw: string | null | undefined): string;
|
|
|
329
329
|
* the console sends no Referer to name the page the form was on. Checked by passwordSetNext; else home.
|
|
330
330
|
*/
|
|
331
331
|
export declare function refusalNext(method: string, url: URL): string;
|
|
332
|
+
/**
|
|
333
|
+
* Whether an act asked for its answer in place (live-pages-and-home.md B2.3): `x-toolroll-act: in-place` turns the
|
|
334
|
+
* answer into a small JSON outcome (`{ outcome, words }`) instead of a page or a redirect. It chooses only the answer's
|
|
335
|
+
* shape: a handler reads it after every proof and authorisation decision is made, and no decision ever reads it.
|
|
336
|
+
*/
|
|
337
|
+
export declare function answersInPlace(request: IncomingMessage | null | undefined): boolean;
|
package/dist/server/http.js
CHANGED
|
@@ -315,3 +315,11 @@ export function refusalNext(method, url) {
|
|
|
315
315
|
}
|
|
316
316
|
return "/";
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Whether an act asked for its answer in place (live-pages-and-home.md B2.3): `x-toolroll-act: in-place` turns the
|
|
320
|
+
* answer into a small JSON outcome (`{ outcome, words }`) instead of a page or a redirect. It chooses only the answer's
|
|
321
|
+
* shape: a handler reads it after every proof and authorisation decision is made, and no decision ever reads it.
|
|
322
|
+
*/
|
|
323
|
+
export function answersInPlace(request) {
|
|
324
|
+
return request?.headers["x-toolroll-act"] === "in-place";
|
|
325
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which pages may change in place, and which may be read ahead (docs/plans/live-pages-and-home.md A1.1, A1.4).
|
|
3
|
+
*
|
|
4
|
+
* Both are explicit allowlists of route-table ids (route-table.ts): nothing is eligible by default, so a new route
|
|
5
|
+
* loads as its own document until it is added here on purpose. The welcome and first-run pages, the terminal, sign-in,
|
|
6
|
+
* the release sheet and every page not named below are never eligible. A page on the in-place list is still a document
|
|
7
|
+
* load when it is sensitive (a password field) or carries a page script (serve.ts sendScreen decides `inPlace`).
|
|
8
|
+
*/
|
|
9
|
+
/** Home, the Tasks list, a task page, Projects, Agents (the list), Search and the Settings pages. */
|
|
10
|
+
export declare const IN_PLACE_ROUTES: ReadonlySet<string>;
|
|
11
|
+
/** What may be read ahead with `x-toolroll-read: prefetch`: Home, the Tasks list, Projects, Agents (the list, never an
|
|
12
|
+
* opened agent), Search, a task page. Everything else answers 204. */
|
|
13
|
+
export declare const PREFETCH_ROUTES: ReadonlySet<string>;
|
|
14
|
+
export declare function inPlaceRoute(id: string | undefined, url: URL): boolean;
|
|
15
|
+
export declare function prefetchRoute(id: string | undefined, url: URL): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which pages may change in place, and which may be read ahead (docs/plans/live-pages-and-home.md A1.1, A1.4).
|
|
3
|
+
*
|
|
4
|
+
* Both are explicit allowlists of route-table ids (route-table.ts): nothing is eligible by default, so a new route
|
|
5
|
+
* loads as its own document until it is added here on purpose. The welcome and first-run pages, the terminal, sign-in,
|
|
6
|
+
* the release sheet and every page not named below are never eligible. A page on the in-place list is still a document
|
|
7
|
+
* load when it is sensitive (a password field) or carries a page script (serve.ts sendScreen decides `inPlace`).
|
|
8
|
+
*/
|
|
9
|
+
/** The Settings pages (route-table.ts): the overview, its groups and each section page. Not the Slack manifest file. */
|
|
10
|
+
const SETTINGS_PAGES = [
|
|
11
|
+
"settings.page",
|
|
12
|
+
...["general", "agents", "alerts", "projects", "access", "system"].map(one => `settings.group-${one}`),
|
|
13
|
+
...["models", "tools", "project", "approval", "policy", "sessions", "integrations", "monitoring", "updates", "retention", "storage",
|
|
14
|
+
"pull-requests", "checks", "backups", "data", "sign-in", "lead", "teams", "discord", "slack",
|
|
15
|
+
"skills", "flows", "knowledge", "learning", "spend", "passwordless"].map(one => `settings.${one}`),
|
|
16
|
+
];
|
|
17
|
+
/** Home, the Tasks list, a task page, Projects, Agents (the list), Search and the Settings pages. */
|
|
18
|
+
export const IN_PLACE_ROUTES = new Set(["home", "tasks", "task.page", "projects.page", "agents", "search", ...SETTINGS_PAGES]);
|
|
19
|
+
/** What may be read ahead with `x-toolroll-read: prefetch`: Home, the Tasks list, Projects, Agents (the list, never an
|
|
20
|
+
* opened agent), Search, a task page. Everything else answers 204. */
|
|
21
|
+
export const PREFETCH_ROUTES = new Set(["home", "tasks", "projects.page", "agents", "search", "task.page"]);
|
|
22
|
+
/** The Agents view's Live grid shares the `agents` row (`/agents?view=live`); it only watches, and is never in place. */
|
|
23
|
+
const liveGrid = (id, url) => id === "agents" && url.searchParams.get("view") === "live";
|
|
24
|
+
export function inPlaceRoute(id, url) {
|
|
25
|
+
return id !== undefined && IN_PLACE_ROUTES.has(id) && !liveGrid(id, url);
|
|
26
|
+
}
|
|
27
|
+
/** An opened agent shares the `agents` row too (`/agents?run=`): opening one marks it seen or watched, which is an
|
|
28
|
+
* act, so its page is never read ahead (the browser never shows it before the person's own read). */
|
|
29
|
+
const openedAgent = (id, url) => id === "agents" && url.searchParams.has("run");
|
|
30
|
+
export function prefetchRoute(id, url) {
|
|
31
|
+
return id !== undefined && PREFETCH_ROUTES.has(id) && !liveGrid(id, url) && !openedAgent(id, url);
|
|
32
|
+
}
|
package/dist/server/kit.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* as plain HTML without it.
|
|
10
10
|
*/
|
|
11
11
|
import { type AttributeValue, type Html, type HtmlValue, type PostFormOptions } from "../html.js";
|
|
12
|
-
import { type ApprovalFacts, type ApprovalKind, type ChipTone, type KitIcon, type StatusKey, type StepItem, type TimingRow } from "../design/kit.js";
|
|
12
|
+
import { type ApprovalFacts, type ApprovalKind, type ChipTone, type KitIcon, type StatusKey, type StepItem, type StepRowMark, type TimingRow } from "../design/kit.js";
|
|
13
13
|
import type { Headline } from "../task-status.js";
|
|
14
14
|
type Attrs = Readonly<Record<string, AttributeValue>>;
|
|
15
15
|
export declare function icon(name: KitIcon, small?: boolean): Html;
|
|
@@ -140,7 +140,7 @@ export declare function facts(rows: ReadonlyArray<readonly [key: string, value:
|
|
|
140
140
|
export declare const note: (text: HtmlValue) => Html;
|
|
141
141
|
/** A refusal or problem, where it happened (role=alert): what went wrong and what didn't change. */
|
|
142
142
|
export declare const notice: (text: HtmlValue) => Html;
|
|
143
|
-
/** One neutral system line (a builder offline, an update): not a failure, never
|
|
143
|
+
/** One neutral system line (a builder offline, an update): not a failure, never the needs-you mark. */
|
|
144
144
|
export declare const infoLine: (text: HtmlValue) => Html;
|
|
145
145
|
export declare function empty(options: {
|
|
146
146
|
title: string;
|
|
@@ -223,7 +223,7 @@ export declare function approveForm(options: {
|
|
|
223
223
|
}): Html;
|
|
224
224
|
/** The terms line for an act, from the same rules. */
|
|
225
225
|
export declare const approvalTerms: (kind: ApprovalKind, facts: ApprovalFacts) => string;
|
|
226
|
-
/** Waits that aren't tasks, on their own page (a spend limit, a failed backup): one
|
|
226
|
+
/** Waits that aren't tasks, on their own page (a spend limit, a failed backup): one ink outline, a count, items
|
|
227
227
|
* split by hairlines, each with its one action. They never join the global Needs-you count. */
|
|
228
228
|
export declare function needsPanel(items: ReadonlyArray<{
|
|
229
229
|
title: string;
|
|
@@ -359,6 +359,39 @@ export declare function outField(options: {
|
|
|
359
359
|
error?: string | null;
|
|
360
360
|
attrs?: Attrs;
|
|
361
361
|
}): Html;
|
|
362
|
+
/** One step row: its marker, label, one sentence, a time (`at` ISO, `when` the words for it), and its body. `toggle`: a
|
|
363
|
+
* finished row's head is a disclosure button (aria-expanded) for its body; `open` shows the body. */
|
|
364
|
+
export type StepRowItem = {
|
|
365
|
+
key: string;
|
|
366
|
+
label: string;
|
|
367
|
+
mark: StepRowMark;
|
|
368
|
+
sentence: HtmlValue;
|
|
369
|
+
needsYou?: boolean;
|
|
370
|
+
current?: boolean;
|
|
371
|
+
at?: string | null;
|
|
372
|
+
when?: string;
|
|
373
|
+
body?: HtmlValue;
|
|
374
|
+
toggle?: boolean;
|
|
375
|
+
open?: boolean;
|
|
376
|
+
};
|
|
377
|
+
/** The steps of one object as the page itself (a task: Plan · Approval · Build · Checks · Ready): an ordered list, a
|
|
378
|
+
* marker on a rail, the label, one sentence and a time per row, and the row's body under it. The current row is
|
|
379
|
+
* `aria-current="step"`. Same markup as `KStepRows`. */
|
|
380
|
+
export declare function stepRows(items: readonly StepRowItem[], label?: string): Html;
|
|
381
|
+
/** The work's frame (KIT.md "The work"): a header (the title, who is working with a blue dot while it runs, Close), the
|
|
382
|
+
* body that scrolls, and the message box at the foot. The records themselves are followed live by the workspace
|
|
383
|
+
* (`KWork` and `useConversation`); a page without script shows what `body` holds. Same markup as `KWork`. */
|
|
384
|
+
export declare function work(options: {
|
|
385
|
+
id: string;
|
|
386
|
+
title: string;
|
|
387
|
+
who?: HtmlValue;
|
|
388
|
+
live?: boolean;
|
|
389
|
+
close?: string | null;
|
|
390
|
+
hidden?: boolean;
|
|
391
|
+
body?: HtmlValue;
|
|
392
|
+
foot?: HtmlValue;
|
|
393
|
+
attrs?: Attrs;
|
|
394
|
+
}): Html;
|
|
362
395
|
export type CheckStep = {
|
|
363
396
|
key: string;
|
|
364
397
|
title: string;
|
package/dist/server/kit.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* (src/browser/kit/behavior.ts), which wires any kit markup the workspace places; every control here still works
|
|
9
9
|
* as plain HTML without it.
|
|
10
10
|
*/
|
|
11
|
-
import { attributes, html, joinHtml, passwordlessRendering, postForm } from "../html.js";
|
|
12
|
-
import { KIT_ICONS, NOT_FOUND, SETTINGS_GROUPS, STATUS, STEP_WORDS, TIMING_WORDS, approvalAsk, chipOfHeadline, dur, needsWords, stepsFill, termsOf, timingBars, } from "../design/kit.js";
|
|
11
|
+
import { attributes, html, htmlString, joinHtml, passwordlessRendering, postForm } from "../html.js";
|
|
12
|
+
import { KIT_ICONS, NOT_FOUND, SETTINGS_GROUPS, STATUS, STEP_ROW_WORDS, STEP_WORDS, stepRowBodyId, TIMING_WORDS, approvalAsk, chipOfHeadline, dur, needsWords, stepsFill, termsOf, timingBars, } from "../design/kit.js";
|
|
13
13
|
const cls = (...names) => names.filter(Boolean).join(" ");
|
|
14
14
|
// ---- text and icons ---------------------------------------------------------------------------------------------
|
|
15
15
|
export function icon(name, small = false) {
|
|
@@ -106,7 +106,7 @@ export function facts(rows) {
|
|
|
106
106
|
export const note = (text) => html `<p class="k-note">${text}</p>`;
|
|
107
107
|
/** A refusal or problem, where it happened (role=alert): what went wrong and what didn't change. */
|
|
108
108
|
export const notice = (text) => html `<p class="k-notice" role="alert">${icon("alert", true)}<span>${text}</span></p>`;
|
|
109
|
-
/** One neutral system line (a builder offline, an update): not a failure, never
|
|
109
|
+
/** One neutral system line (a builder offline, an update): not a failure, never the needs-you mark. */
|
|
110
110
|
export const infoLine = (text) => html `<p class="k-notice info">${text}</p>`;
|
|
111
111
|
export function empty(options) {
|
|
112
112
|
return html `<div class="k-empty"><p class="k-empty-t">${options.title}</p>${options.text ? html `<p class="k-empty-x">${options.text}</p>` : ""}${options.action ?? ""}</div>`;
|
|
@@ -180,7 +180,7 @@ export function approveForm(options) {
|
|
|
180
180
|
}
|
|
181
181
|
/** The terms line for an act, from the same rules. */
|
|
182
182
|
export const approvalTerms = (kind, facts) => termsOf(kind, facts);
|
|
183
|
-
/** Waits that aren't tasks, on their own page (a spend limit, a failed backup): one
|
|
183
|
+
/** Waits that aren't tasks, on their own page (a spend limit, a failed backup): one ink outline, a count, items
|
|
184
184
|
* split by hairlines, each with its one action. They never join the global Needs-you count. */
|
|
185
185
|
export function needsPanel(items, label = "Needs you") {
|
|
186
186
|
if (items.length === 0)
|
|
@@ -320,6 +320,35 @@ export function outField(options) {
|
|
|
320
320
|
return html `<div class="k-field"><label class="k-label" for="${options.id}">${options.label}</label>${control}\
|
|
321
321
|
${options.hint ? html `<p class="k-hint" id="${options.id}-hint">${options.hint}</p>` : ""}${options.error ? html `<p class="k-err" id="${options.id}-err" role="alert">${options.error}</p>` : ""}</div>`;
|
|
322
322
|
}
|
|
323
|
+
/** The steps of one object as the page itself (a task: Plan · Approval · Build · Checks · Ready): an ordered list, a
|
|
324
|
+
* marker on a rail, the label, one sentence and a time per row, and the row's body under it. The current row is
|
|
325
|
+
* `aria-current="step"`. Same markup as `KStepRows`. */
|
|
326
|
+
export function stepRows(items, label = "Steps") {
|
|
327
|
+
return html `<ol class="k-steprows" aria-label="${label}">${items.map(item => {
|
|
328
|
+
const hasBody = item.body !== undefined && htmlString(html `${item.body}`) !== "";
|
|
329
|
+
const id = stepRowBodyId(item.key);
|
|
330
|
+
const head = html `<span class="k-steprow-label">${item.label}<span class="k-sr">, ${STEP_ROW_WORDS[item.mark]}</span></span>\
|
|
331
|
+
<span class="k-steprow-say">${item.needsYou ? chip("Needs you", "needs") : ""}${item.sentence === "" ? "" : html `<span>${item.sentence}</span>`}</span>\
|
|
332
|
+
${item.at ? html `<time class="k-steprow-time k-num" datetime="${item.at}">${item.when ?? ""}</time>` : ""}`;
|
|
333
|
+
return html `<li class="${cls("k-steprow", item.mark)}" data-step-row="${item.key}"${item.current ? html ` aria-current="step"` : ""}><span class="k-steprow-mark" aria-hidden="true"></span>\
|
|
334
|
+
<div class="k-steprow-main">${item.toggle && hasBody
|
|
335
|
+
? html `<button class="k-steprow-head" type="button" aria-expanded="${item.open ? "true" : "false"}" aria-controls="${id}">${head}</button>`
|
|
336
|
+
: html `<div class="k-steprow-head">${head}</div>`}${hasBody ? html `<div class="k-steprow-body" id="${id}"${item.open ? "" : html ` hidden`}>${item.body}</div>` : ""}</div></li>`;
|
|
337
|
+
})}</ol>`;
|
|
338
|
+
}
|
|
339
|
+
// ---- the work -----------------------------------------------------------------------------------------------------
|
|
340
|
+
/** The work's frame (KIT.md "The work"): a header (the title, who is working with a blue dot while it runs, Close), the
|
|
341
|
+
* body that scrolls, and the message box at the foot. The records themselves are followed live by the workspace
|
|
342
|
+
* (`KWork` and `useConversation`); a page without script shows what `body` holds. Same markup as `KWork`. */
|
|
343
|
+
export function work(options) {
|
|
344
|
+
const who = options.who === undefined || htmlString(html `${options.who}`) === "" ? ""
|
|
345
|
+
: html `<p class="k-work-who">${options.live ? html `<span class="k-work-dot" aria-hidden="true"></span>` : ""}<span>${options.who}</span></p>`;
|
|
346
|
+
const close = options.close === undefined || options.close === null ? "" : btn(options.close, { variant: "ghost", sm: true, iconOnly: "x" });
|
|
347
|
+
const foot = options.foot === undefined || htmlString(html `${options.foot}`) === "" ? "" : html `<div class="k-work-foot">${options.foot}</div>`;
|
|
348
|
+
return html `<aside class="k-work" id="${options.id}" aria-labelledby="${options.id}-h"${options.hidden ? html ` hidden` : ""}${attributes(options.attrs ?? {})}>\
|
|
349
|
+
<header class="k-work-head"><div class="k-work-title"><h2 id="${options.id}-h">${options.title}</h2>${who}</div>${close}</header>\
|
|
350
|
+
<div class="k-work-body" data-agent-scroll="">${options.body ?? ""}</div>${foot}</aside>`;
|
|
351
|
+
}
|
|
323
352
|
/** A short list of steps: a mark, the step and one line, its one action. Done steps read muted with a check; a step
|
|
324
353
|
* being checked shows a static ring (running state never animates). The state is also said in words. */
|
|
325
354
|
export function checklist(steps, label = "Steps") {
|
package/dist/server/pages.js
CHANGED
|
@@ -72,7 +72,7 @@ import { rereadBrowser } from "./live.js";
|
|
|
72
72
|
import { homeView } from "./home.js";
|
|
73
73
|
import { workIndexPage } from "../work-index.js";
|
|
74
74
|
export function createPagesHandlers(runtime) {
|
|
75
|
-
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession, firstRunStepsNow } = runtime;
|
|
75
|
+
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession, firstRunStepsNow, chatEnablement, leadPrincipal, leadConversationRows } = runtime;
|
|
76
76
|
// The filesystem browser (operator request): pick a project folder by
|
|
77
77
|
// looking, not typing. CONFINED to exactly what opening allows —
|
|
78
78
|
// explicit --repo mode has an enumerable list and gets no browser at
|
|
@@ -203,7 +203,9 @@ export function createPagesHandlers(runtime) {
|
|
|
203
203
|
const door = consentDoorOf(scope, runtime.routeViewOf(taskId, store.lookupRef(taskId), scope, now, who));
|
|
204
204
|
return door.open ? null : door;
|
|
205
205
|
};
|
|
206
|
-
const
|
|
206
|
+
const chat = chrome.chat === true;
|
|
207
|
+
const view = { ...homeView({ store, now, access: workAccess(), evidenceRoot, project, projects, chat, canAnswer: who.role === "approver", closedDoor,
|
|
208
|
+
leadMessage: chat ? leadLastMessage(who, now) : null }),
|
|
207
209
|
// Until the first result: the welcome's three steps stay one link away.
|
|
208
210
|
setup: firstRunStepsNow(now) !== null ? "/welcome" : null };
|
|
209
211
|
return sendScreen(response, 200, screen("Home", html `<div data-kit-page><h1>Home</h1><p>${view.say}</p><p><a href="/tasks">Tasks</a></p></div>`, { chrome, workspace: { view } }));
|
|
@@ -1205,5 +1207,24 @@ export function createPagesHandlers(runtime) {
|
|
|
1205
1207
|
"edge.font": fontAsset,
|
|
1206
1208
|
"edge.desktop-health": desktopHealth,
|
|
1207
1209
|
});
|
|
1210
|
+
/** The lead's last message in this person's main thread, for Home's one line above the message box. The thread is
|
|
1211
|
+
* read only through the gate a docked conversation uses (serve.ts dockedConversation): an approver in a browser
|
|
1212
|
+
* session, chat enabled, the approver's standing re-proved now (leadPrincipal), their lead session under the current
|
|
1213
|
+
* ceiling and generation; and then through leadConversationRows, the very rows /chat renders, which keep only a thread
|
|
1214
|
+
* under this person's current ceiling. Null at any refusal, and when the lead has said nothing yet. */
|
|
1215
|
+
function leadLastMessage(who, now) {
|
|
1216
|
+
if (who.role !== "approver")
|
|
1217
|
+
return null;
|
|
1218
|
+
const enabled = chatEnablement();
|
|
1219
|
+
const principal = enabled.ok ? leadPrincipal(who) : null;
|
|
1220
|
+
if (!enabled.ok || principal === null)
|
|
1221
|
+
return null;
|
|
1222
|
+
const found = store.activeLeadSession(who.name);
|
|
1223
|
+
const notStarted = (found === null || found.ceilingDigest !== principal.ceilingDigest) && enabled.billing === "subscription";
|
|
1224
|
+
if (!notStarted && (found === null || found.ceilingDigest !== principal.ceilingDigest || found.approverGeneration !== principal.generation))
|
|
1225
|
+
return null;
|
|
1226
|
+
const said = leadConversationRows(who, principal, null, now, null).messages.filter(one => one.role === "assistant" && one.pending !== true).at(-1);
|
|
1227
|
+
return said === undefined ? null : { text: said.text, at: said.createdAt };
|
|
1228
|
+
}
|
|
1208
1229
|
return { registrations, projectsScreen };
|
|
1209
1230
|
}
|
|
@@ -84,6 +84,8 @@ export type TaskChatFocus = {
|
|
|
84
84
|
origin?: TaskOrigin | null;
|
|
85
85
|
/** The repair chain row when this task is a machine-drafted repair. */
|
|
86
86
|
repairChain: RepairChainRow | null;
|
|
87
|
+
/** A release filed while its pull request's checks still ran: the sheet says nothing deploys until they pass. */
|
|
88
|
+
releaseCiRunning?: boolean;
|
|
87
89
|
} | null;
|
|
88
90
|
decisions: (Decision & {
|
|
89
91
|
taskId: string;
|
|
@@ -93,6 +93,7 @@ export function taskChatApproval(focus, csrf) {
|
|
|
93
93
|
coordinator: approval.coordinator,
|
|
94
94
|
origin: approval.origin ?? null,
|
|
95
95
|
deliverable: approval.deliverable,
|
|
96
|
+
releaseCiRunning: approval.releaseCiRunning === true,
|
|
96
97
|
rebuild: focus.assignment?.need != null && "key" in focus.assignment.need && focus.assignment.need.key === "replan" ? focus.assignment.need.build : null,
|
|
97
98
|
// Edit plan opens the task page's sheet with its fields open for editing.
|
|
98
99
|
editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
|
|
@@ -129,7 +129,7 @@ export function boardBody(data, ciRed) {
|
|
|
129
129
|
return `${Math.round(minutes / 60)}h`;
|
|
130
130
|
return `${Math.round(minutes / (24 * 60))}d`;
|
|
131
131
|
};
|
|
132
|
-
// A lane's head is a status chip in its own tone (after ReUI's badge with a dot):
|
|
132
|
+
// A lane's head is a status chip in its own tone (after ReUI's badge with a dot): solid ink only for the lane that
|
|
133
133
|
// waits on a person, blue for building, green for done, neutral otherwise. The word carries the state.
|
|
134
134
|
const LANE_CHIP = { attention: "chip-needs", building: "chip-live", done: "chip-done", queued: "", waiting: "" };
|
|
135
135
|
const laneChip = (key, title) => html `<span class="chip ${LANE_CHIP[key]}"><span class="chip-dot" aria-hidden="true"></span>${title}</span>`;
|
|
@@ -70,7 +70,7 @@ export function editorFileHref(worktree, path, line) {
|
|
|
70
70
|
* chat (v39, extending Priority 2's "one surface, six places" rule from
|
|
71
71
|
* the verdict word to the per-criterion state). */
|
|
72
72
|
export function matrixStateBadge(state) {
|
|
73
|
-
// Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an
|
|
73
|
+
// Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an ink note.
|
|
74
74
|
const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
|
|
75
75
|
// The same words as the Checks tab and the card's Requirements row (task-status.ts).
|
|
76
76
|
return html `<span class="badge ${cls}" data-matrix-state="${state}">${requirementWordOf({ state })}</span>`;
|
|
@@ -828,7 +828,7 @@ export function pullRequestCardHtml(pr, csrf) {
|
|
|
828
828
|
const view = pr.view;
|
|
829
829
|
const url = safePrUrl(view.prUrl);
|
|
830
830
|
const link = view.prNumber === null ? "" : url === null ? html `<span class="mono">PR #${view.prNumber}</span>` : html `<a href="${url}" class="mono" rel="noreferrer" target="_blank">PR #${view.prNumber}</a>`;
|
|
831
|
-
// A pull request's trouble never undoes the result:
|
|
831
|
+
// A pull request's trouble never undoes the result: an ink note, never red.
|
|
832
832
|
const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" ? "problem" : "waiting";
|
|
833
833
|
const revision = view.revisionTask === null || view.state !== "failing" ? "" : html ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
|
|
834
834
|
const merged = view.mergeCommit === null ? "" : html `<p class="meta">Merge commit <span class="mono">${view.mergeCommit.slice(0, 12)}</span></p>`;
|
|
@@ -1507,7 +1507,7 @@ export function completionReceiptCard(view, taskId, place, standing = receiptSta
|
|
|
1507
1507
|
lead,
|
|
1508
1508
|
html `<p class="receipt-summary">${conciseOutcomeOf(view.summary ?? (view.outcome === "no-change" ? "The agent found that no repository change was needed." : "The build finished without a concise handoff."))}</p>`,
|
|
1509
1509
|
headline ? html `<div class="receipt-actions"><a class="button-link" href="${resultHref}" data-open-result data-primary-action>${status.action?.label ?? "Open result"}</a></div>` : "",
|
|
1510
|
-
// A pull request that couldn't open never undoes the result: an
|
|
1510
|
+
// A pull request that couldn't open never undoes the result: an ink note, not red.
|
|
1511
1511
|
view.publication?.state === "failed" ? html `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "",
|
|
1512
1512
|
caveats,
|
|
1513
1513
|
coverage,
|
|
@@ -122,6 +122,8 @@ export type ApprovalSheetInput = {
|
|
|
122
122
|
freshSince?: string | null;
|
|
123
123
|
/** Easy-start C5: a laptop's local owner — eligible (no password field), or why not and where to go. */
|
|
124
124
|
localOwner?: LocalApprovalView | null;
|
|
125
|
+
/** A release filed while its pull request's checks still ran: the plan sheet says nothing deploys until they pass. */
|
|
126
|
+
releaseCiRunning?: boolean;
|
|
125
127
|
};
|
|
126
128
|
export type LocalApprovalView = {
|
|
127
129
|
eligible: true;
|
|
@@ -147,7 +149,15 @@ export declare function approvalSheetParts(input: ApprovalSheetInput): {
|
|
|
147
149
|
after: Html;
|
|
148
150
|
allowing: string[];
|
|
149
151
|
passwordNote: string;
|
|
152
|
+
/** The same terms as `lead`, `rows` and `details`, re-ordered to read top to bottom (`planSheetHtml`), nothing folded. */
|
|
153
|
+
sheet: Html;
|
|
154
|
+
/** The sheet's lead sentence (the approach's first sentence, else the goal), for a one-line summary of the plan. */
|
|
155
|
+
sheetLead: string;
|
|
150
156
|
};
|
|
157
|
+
/** The plan sheet: what an approval signs, read top to bottom (approvalSheetParts' own parts, re-ordered). */
|
|
158
|
+
export declare function planSheetHtml(input: ApprovalSheetInput): Html;
|
|
159
|
+
/** Evidence kinds on the plan sheet: how each requirement is proven, in plain words. */
|
|
160
|
+
export declare const EVIDENCE_SHEET: Record<string, string>;
|
|
151
161
|
/** Earlier versions of a task still in flight, in plain words: queued
|
|
152
162
|
* while they only wait, running once one has started; null when none. */
|
|
153
163
|
export declare function earlierVersionsWords(active: number, running: number): string | null;
|