toolroll 0.9.86 → 0.9.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +722 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +31 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +38 -4
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +23 -0
- package/dist/release.js +46 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +24 -12
- package/dist/server/flows.js +64 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +70 -0
- package/dist/server/kit.d.ts +419 -0
- package/dist/server/kit.js +369 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +22 -190
- package/dist/server/render-pages.js +123 -575
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { requestLimitsHtml, tokenLimitWords } from "../request-budget-ui.js";
|
|
|
10
10
|
import { authenticateAccount } from "../scope.js";
|
|
11
11
|
import { SSO_CALLBACK } from "../sso-settings.js";
|
|
12
12
|
import { personChip, refuse, screen } from "./chrome.js";
|
|
13
|
+
import { inviteLinkHtml, ownAccessHtml, peopleHtml, settingsSubPage } from "../settings-groups-ui.js";
|
|
13
14
|
import { redirect, respond, sayTo } from "./http.js";
|
|
14
15
|
/** people handlers, moved without changing their route bodies. */
|
|
15
16
|
import { randomBytes } from "node:crypto";
|
|
@@ -23,6 +24,7 @@ import { cursorOf as auditCursorOf, remoteAudit } from "../remote-audit.js";
|
|
|
23
24
|
import { credentialProof, hashPassword, proveAccountNow } from "../scope.js";
|
|
24
25
|
import { form, HANDOFF_STYLE, loginHref, loginReturn, page, SIGN_IN_COOKIE, signInSpent, startedHere } from "./http.js";
|
|
25
26
|
import { joinDeadPage, joinFormPage, loginPage, SIGNUP_NAME, SIGNUP_PASSWORD_MIN, signupPage } from "./render-people.js";
|
|
27
|
+
import { liveInviteTerms } from "../store.js";
|
|
26
28
|
import { SESSION_COOKIE, SIGN_IN_LINK_PATH } from "./session.js";
|
|
27
29
|
import { setupCodeMatches, signupCookie, signupFromHere } from "./signup-gate.js";
|
|
28
30
|
/** Clock skew allowed between the provider and this server when reading auth_time — forward only (a provider clock
|
|
@@ -51,7 +53,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
51
53
|
const view = { who: who.name, everyone, canSeeEveryone: store.isInstanceOperator(who.name), now: Date.now(),
|
|
52
54
|
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name),
|
|
53
55
|
limits: { note: tokenLimitWords.bind(null, store.handle), section: store.isInstanceOperator(who.name) ? requestLimitsHtml(store.handle, store.apiTokens(null), Date.now()) : html `` } };
|
|
54
|
-
return sendScreen(response, 200, screen("Sessions & tokens",
|
|
56
|
+
return sendScreen(response, 200, screen("Sessions & tokens", settingsSubPage("access", "Sessions and tokens", html `${credentialsHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`), { chrome: chromeFor(project, "settings") }));
|
|
55
57
|
}
|
|
56
58
|
async function peoplePage(ctx) {
|
|
57
59
|
const { url, who, response, now, project } = ctx;
|
|
@@ -74,7 +76,7 @@ export function createPeopleHandlers(runtime) {
|
|
|
74
76
|
// the process admission); a viewer sees exactly themselves (U3).
|
|
75
77
|
if (restricted()) {
|
|
76
78
|
const projects = admissionList() ?? [];
|
|
77
|
-
return sendScreen(response, 200, screen("
|
|
79
|
+
return sendScreen(response, 200, screen("Your access", ownAccessHtml({ name: who.name, approver: who.role === "approver", projects: projects.map(repo => projectName(repo)), activity: personHref(who.name) }), { chrome: chromeFor(project, "people") }));
|
|
78
80
|
}
|
|
79
81
|
const approverView = store.isInstanceOperator(who.name);
|
|
80
82
|
const accounts = store.accountFacts().filter(one => approverView || one.name === who.name);
|
|
@@ -87,79 +89,25 @@ export function createPeopleHandlers(runtime) {
|
|
|
87
89
|
return seen;
|
|
88
90
|
};
|
|
89
91
|
const projectChoices = consoleProjects();
|
|
90
|
-
const
|
|
91
|
-
<p class="meta">Operators with all-project access also manage the instance.</p><fieldset><legend>Projects</legend>${projectChoices.length === 0 ? html `<p class="meta">Add a project before granting selected access.</p>` : projectChoices.map(repo => html `<label class="row"><input type="checkbox" name="projects" value="${repo}"${selected?.includes(repo) ? " checked" : ""}>${projectName(repo)}</label>`)}</fieldset>`;
|
|
92
|
-
const cards = accounts.map(one => {
|
|
92
|
+
const people = accounts.map(one => {
|
|
93
93
|
const seen = lastSeenOf(one.name);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
?
|
|
97
|
-
: one.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
html `<h2 style="margin-top:0">${personChip(one.name)} <span class="meta">${standing}</span></h2>`,
|
|
103
|
-
html `<p class="meta">${one.projects === null ? "All projects" : one.projects.length === 0 ? "No project access" : one.projects.map(repo => projectName(repo)).join(", ")}</p>`,
|
|
104
|
-
approverView && one.revokedAt === null ? html `<details><summary>Edit project access</summary>${postForm("/people/projects", html `${accessFields(one.projects)}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><p class="meta">Changing access signs this person out and ends their derived sessions and modes. Previously approved work stays recorded.</p><button type="submit">Save project access</button>`, { hidden: { name: one.name } })}</details>` : "",
|
|
105
|
-
html `<p class="meta">${seen === null ? "not signed in right now" : `signed in \u2014 active ${new Date(seen).toISOString().slice(11, 16)} UTC`} \u00b7 joined ${one.addedAt.slice(0, 10)}</p>`,
|
|
106
|
-
html `<p><a href="${personHref(one.name)}">Tokens and remote activity</a></p>`,
|
|
107
|
-
acts.length === 0
|
|
108
|
-
? html `<p class="meta">No recorded acts yet</p>`
|
|
109
|
-
: joinHtml(acts.map(act => html `<p class="row meta">${act.kind} ${act.subject} \u00b7 ${act.at.slice(0, 16).replace("T", " ")}</p>`), "\n"),
|
|
110
|
-
approverView && one.revokedAt === null && one.name !== who.name
|
|
111
|
-
? postForm("/people/revoke", html `\
|
|
112
|
-
${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`)}\
|
|
113
|
-
<button type="submit">Remove ${one.name}'s sign-in</button>\
|
|
114
|
-
<span class="meta">ends their access and everything it signed \u2014 history stays</span>`, { attrs: { class: "row" }, hidden: { name: one.name } })
|
|
115
|
-
: "",
|
|
116
|
-
html `</div>`,
|
|
117
|
-
], "\n");
|
|
94
|
+
return {
|
|
95
|
+
name: one.name, you: one.name === who.name, approver: one.role === "approver", projects: one.projects,
|
|
96
|
+
projectNames: one.projects === null ? "All projects" : one.projects.length === 0 ? "No project access" : one.projects.map(repo => projectName(repo)).join(", "),
|
|
97
|
+
revoked: one.revokedAt === null ? null : `Removed ${one.revokedAt.slice(0, 16).replace("T", " ")} by ${one.revokedBy ?? "someone"}`,
|
|
98
|
+
seen: seen === null ? `Joined ${one.addedAt.slice(0, 10)}` : now.getTime() - seen < 5 * 60_000 ? "Signed in now" : `Active ${new Date(seen).toISOString().slice(11, 16)} UTC`,
|
|
99
|
+
joined: one.addedAt.slice(0, 10), activity: personHref(one.name),
|
|
100
|
+
acts: store.recentActsOf(one.name).map(act => `${act.kind} ${act.subject} ${act.at.slice(0, 16).replace("T", " ")}`),
|
|
101
|
+
};
|
|
118
102
|
});
|
|
119
103
|
const invites = approverView ? store.openInvites(now) : [];
|
|
120
|
-
const
|
|
121
|
-
${
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
?
|
|
126
|
-
|
|
127
|
-
html `<div class="card">`,
|
|
128
|
-
html `<h2 style="margin-top:0">Invite someone</h2>`,
|
|
129
|
-
inviteRows.length === 0 ? html `<p class="meta">No open invites</p>` : joinHtml(inviteRows, "\n"),
|
|
130
|
-
postForm("/people/invite", joinHtml([
|
|
131
|
-
"",
|
|
132
|
-
html `<label>They can<select name="role"><option value="viewer">Viewer — read work</option><option value="approver">Operator — create, manage and approve work</option></select></label>`,
|
|
133
|
-
accessFields(project === null ? [] : [project]),
|
|
134
|
-
html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
135
|
-
html `<button type="submit">Make an invite link</button>`,
|
|
136
|
-
html `<span class="meta">single-use, expires in 72 hours</span>`,
|
|
137
|
-
], "\n"), { attrs: { class: "row" } }),
|
|
138
|
-
html `</div>`,
|
|
139
|
-
], "\n");
|
|
140
|
-
// Coordinators (MCP spec v6): the machine principals beside the
|
|
141
|
-
// human ones — name, immutable fingerprint, what they may file
|
|
142
|
-
// into, their rate, and the revoke road. Approver eyes only.
|
|
143
|
-
const coordinatorCard = !approverView
|
|
144
|
-
? ""
|
|
145
|
-
: (() => {
|
|
146
|
-
const rows = listCoordinators(store);
|
|
147
|
-
if (rows.length === 0)
|
|
148
|
-
return "";
|
|
149
|
-
return joinHtml([
|
|
150
|
-
html `<div class="card">`,
|
|
151
|
-
html `<h2 style="margin-top:0">Agent filers</h2>`,
|
|
152
|
-
html `<p class="meta">Agents that can propose tasks. You approve what runs.</p>`,
|
|
153
|
-
...rows.map(one => html `<p class="row"><span class="mono">${`${one.name}#${one.cid.slice(0, 4)}`}</span> ${one.revokedAt !== null
|
|
154
|
-
? html `<span class="meta">revoked ${one.revokedAt.slice(0, 10)}</span>`
|
|
155
|
-
: html `<span class="meta">${one.perHour}/hour · files into ${one.repos.join(", ")} · ${one.expiresAt === null ? "no expiry (made before expiry)" : `expires ${one.expiresAt.slice(0, 10)}`} · last filed ${one.lastFiledAt === null ? "never" : one.lastFiledAt.slice(0, 16).replace("T", " ")}</span>`}</p>`),
|
|
156
|
-
html `<details class="settings-more"><summary>Revoke an agent filer</summary><p class="meta">Run <code>toolroll coordinator revoke <cid> --as you</code> on this computer.</p></details>`,
|
|
157
|
-
html `</div>`,
|
|
158
|
-
], "\n");
|
|
159
|
-
})();
|
|
160
|
-
return sendScreen(response, 200, screen("people", joinHtml([html `<h1>People</h1>`, ...cards, coordinatorCard, inviteCard], "\n"), {
|
|
161
|
-
chrome: chromeFor(project, "people"),
|
|
162
|
-
}));
|
|
104
|
+
const filers = !approverView ? [] : listCoordinators(store).map(one => ({ name: `${one.name}#${one.cid.slice(0, 4)}`, words: one.revokedAt !== null ? `Revoked ${one.revokedAt.slice(0, 10)}`
|
|
105
|
+
: `${one.perHour} an hour · files into ${one.repos.join(", ")} · ${one.expiresAt === null ? "no expiry (made before expiry)" : `expires ${one.expiresAt.slice(0, 10)}`} · last filed ${one.lastFiledAt === null ? "never" : one.lastFiledAt.slice(0, 16).replace("T", " ")}` }));
|
|
106
|
+
return sendScreen(response, 200, screen("People", peopleHtml({
|
|
107
|
+
operator: approverView, you: who.name, said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), people,
|
|
108
|
+
invites: invites.map(one => ({ id: one.id, role: one.role, projects: one.projects === null ? "All projects" : one.projects.map(repo => projectName(repo)).join(", "), from: one.mintedBy, expires: `${one.expiresAt.slice(0, 16).replace("T", " ")} UTC`, attempts: one.attempts })),
|
|
109
|
+
projects: projectChoices.map(repo => ({ repo, name: projectName(repo) })), openProject: project !== null && projectChoices.includes(project) ? project : null, filers,
|
|
110
|
+
}), { chrome: chromeFor(project, "people") }));
|
|
163
111
|
}
|
|
164
112
|
async function sessionsSend(ctx) {
|
|
165
113
|
const { who, request, response, now, posted } = ctx;
|
|
@@ -258,15 +206,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
258
206
|
return refuse(response, who, 400, "Select at least one project for this invitation.", "/people");
|
|
259
207
|
const minted = store.mintInvite(role, who.name, now, undefined, access.projects);
|
|
260
208
|
const origin = options.publicUrl !== undefined ? options.publicUrl.replace(/\/$/, "") : `http://${request.headers.host ?? "this-console"}`;
|
|
261
|
-
const linkScreen = screen("
|
|
262
|
-
html `<h1>Invite link</h1>`,
|
|
263
|
-
html `<div class="card">`,
|
|
264
|
-
html `<p>Shown once, so copy it now.</p>`,
|
|
265
|
-
html `<p class="mono secret-value">${`${origin}/join/${minted.token}`}</p>`,
|
|
266
|
-
html `<p class="meta">Send it to one person. It works once, lets them ${role === "approver" ? "approve and act" : "read work"} in ${access.projects === null ? "all projects" : access.projects.map(repo => projectName(repo)).join(", ")}, and expires ${minted.expiresAt.slice(0, 16).replace("T", " ")} UTC. Cancel it any time on People.</p>`,
|
|
267
|
-
html `</div>`,
|
|
268
|
-
html `<p class="meta"><a href="/people">Back to People</a></p>`,
|
|
269
|
-
], "\n"),
|
|
209
|
+
const linkScreen = screen("Invite link", inviteLinkHtml({ link: `${origin}/join/${minted.token}`, approver: role === "approver", projects: access.projects === null ? "all projects" : access.projects.map(repo => projectName(repo)).join(", "), expires: `${minted.expiresAt.slice(0, 16).replace("T", " ")} UTC` }),
|
|
270
210
|
// A one-time secret on screen: no script of any kind rides along.
|
|
271
211
|
{ chrome: chromeFor(projectOf(who, request) ?? null, "people"), forceSensitive: true });
|
|
272
212
|
return sendScreen(response, 200, linkScreen);
|
|
@@ -578,7 +518,8 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
578
518
|
async function joinPage(ctx) {
|
|
579
519
|
const { url, response } = ctx;
|
|
580
520
|
const joinToken = joinCodeOf(url);
|
|
581
|
-
|
|
521
|
+
const terms = liveInviteTerms(store, joinToken, clock());
|
|
522
|
+
return page(response, 200, terms !== null ? joinFormPage(joinToken, null, "", terms) : joinDeadPage());
|
|
582
523
|
}
|
|
583
524
|
async function joinSend(ctx) {
|
|
584
525
|
const { url, request, response } = ctx;
|
|
@@ -612,15 +553,15 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
612
553
|
// Possession of a LIVE token earns real words (D6's disclosure
|
|
613
554
|
// ruling) — the attempt is already spent either way (E3).
|
|
614
555
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(name)) {
|
|
615
|
-
return page(response, 400, joinFormPage(joinToken, "
|
|
556
|
+
return page(response, 400, joinFormPage(joinToken, { field: "name", text: "Use 1\u201340 letters, digits, dots, dashes or underscores, starting with a letter or digit." }, name, liveInviteTerms(store, joinToken, clock())));
|
|
616
557
|
}
|
|
617
558
|
if (password.length < 8) {
|
|
618
|
-
return page(response, 400, joinFormPage(joinToken, "
|
|
559
|
+
return page(response, 400, joinFormPage(joinToken, { field: "password", text: "The password needs at least 8 characters." }, name, liveInviteTerms(store, joinToken, clock())));
|
|
619
560
|
}
|
|
620
561
|
const made = store.consumeInviteAndCreateAccount({ tokenValue: joinToken, name, credentialHash: hashPassword(password) }, clock());
|
|
621
562
|
if (!made.ok) {
|
|
622
563
|
if (made.reason === "name-taken") {
|
|
623
|
-
return page(response, 400, joinFormPage(joinToken, "
|
|
564
|
+
return page(response, 400, joinFormPage(joinToken, { field: "name", text: "That name is taken. Choose another." }, name, liveInviteTerms(store, joinToken, clock())));
|
|
624
565
|
}
|
|
625
566
|
return page(response, 200, joinDeadPage());
|
|
626
567
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History pages on the revamp kit (UI revamp wave B): one attempt (/r/<id>), a task's full record (/t/<id>/evidence),
|
|
3
|
+
* an agent's question (/d/<id>). Reading pages: the phone header, then one `doc` column. Rules: src/design/KIT.md.
|
|
4
|
+
*/
|
|
5
|
+
import type { EvidencePack } from "../evidence-pack.js";
|
|
6
|
+
import { type Html, type HtmlValue } from "../html.js";
|
|
7
|
+
import type { RouteStamp } from "../phase-routing.js";
|
|
8
|
+
import { type ResultTab } from "../result-review.js";
|
|
9
|
+
import type { StructuredHandoffView, TerminalDiffView } from "../result-evidence-readers.js";
|
|
10
|
+
import type { Artifact, Decision, DiffComment, Publication, Run } from "../store.js";
|
|
11
|
+
import type { ReviewFacts } from "../workspace-ui.js";
|
|
12
|
+
import { type Chrome, type Screen } from "./chrome.js";
|
|
13
|
+
import { type ProofBundleView, type ResultDetail } from "./render-results.js";
|
|
14
|
+
/** A section of a reading page: its heading and body (kit `k-sec`). */
|
|
15
|
+
export declare const sec: (title: string, body: HtmlValue, attrs?: Html) => Html;
|
|
16
|
+
export declare function runPage(chrome: Chrome, run: Run, taskId: string, artifacts: Artifact[], terminal?: TerminalDiffView | null, notes?: {
|
|
17
|
+
id: number;
|
|
18
|
+
author: string;
|
|
19
|
+
note: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
}[], csrf?: string, comments?: DiffComment[], ciRepair?: {
|
|
22
|
+
pr: number;
|
|
23
|
+
} | null, liveScript?: string, peekable?: boolean, running?: boolean, editor?: {
|
|
24
|
+
worktree: string;
|
|
25
|
+
} | null, editorToggle?: {
|
|
26
|
+
on: boolean;
|
|
27
|
+
} | null, noted?: boolean, structuredHandoff?: StructuredHandoffView | null, proofBundle?: ProofBundleView | null, route?: RouteStamp | null, publicationRow?: Publication | null, review?: ReviewFacts | null, result?: {
|
|
28
|
+
detail: ResultDetail;
|
|
29
|
+
tab: ResultTab;
|
|
30
|
+
user: string;
|
|
31
|
+
requestToken: string;
|
|
32
|
+
} | null, sourceDigest?: string | null): Screen;
|
|
33
|
+
export type QuestionPlace = {
|
|
34
|
+
title: string | null;
|
|
35
|
+
repo: string | null;
|
|
36
|
+
run: Pick<Run, "id" | "role"> | null;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The question an agent asked, on top: what it asked and why, the options as radio cards with its recommendation
|
|
40
|
+
* marked, an optional note, and one Answer (kind none: one click). An irreversible option asks for a tick first; the
|
|
41
|
+
* answer route checks it again. A refusal shows in the box with nothing changed; an answer replaces the box.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decisionPage(chrome: Chrome, decision: Decision, taskId: string, artifacts: Artifact[], now: Date, place: QuestionPlace, returnTo?: string | null, refusal?: string | null,
|
|
44
|
+
/** What a refused answer carried: the choice and the note stay in the box. */
|
|
45
|
+
draft?: {
|
|
46
|
+
choice: string | null;
|
|
47
|
+
note: string | null;
|
|
48
|
+
} | null): Screen;
|
|
49
|
+
/** A task's full record, read-only: what was asked and approved, every agent that ran, the result, and its sealed audit
|
|
50
|
+
* log entries, with the JSON download and Print. The same facts as the pack's JSON (evidence-pack.ts). */
|
|
51
|
+
export declare function evidenceRecordPage(chrome: Chrome, pack: EvidencePack): Screen;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { closedQuestionReason, LIMITS } from "../decision.js";
|
|
2
|
+
import { html, joinHtml, postForm } from "../html.js";
|
|
3
|
+
import { projectName } from "../project.js";
|
|
4
|
+
import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
|
|
5
|
+
import { isOverdue, screen, sentenceCase, whenTime } from "./chrome.js";
|
|
6
|
+
import { taskHref } from "./http.js";
|
|
7
|
+
import { btn, chip, disclose, doc, docHeader, dock, facts, note, notice, phoneHeader, radios, slots, status } from "./kit.js";
|
|
8
|
+
import { editorFileHref, evidenceBundleCard, evidenceWords, humanBytes, prefillScript, resultPanelHtml, revisionSealFields, runFactsRows, runNoun, terminalDiffCard, } from "./render-results.js";
|
|
9
|
+
import { publicationFactsOf } from "./render-tasks.js";
|
|
10
|
+
/** A section of a reading page: its heading and body (kit `k-sec`). */
|
|
11
|
+
export const sec = (title, body, attrs = html ``) => html `<section class="k-sec"${attrs}><h3>${title}</h3>${body}</section>`;
|
|
12
|
+
/** The root every kit server page carries, so the workspace frames it as a kit page (no padding, panel or desk header). */
|
|
13
|
+
const kitPage = (attrs, ...parts) => html `<div class="h-page" data-kit-page${attrs}>${parts}</div>`;
|
|
14
|
+
// ---- /r/<id>: one attempt ----------------------------------------------------------------------------------------
|
|
15
|
+
export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
|
|
16
|
+
const title = `${sentenceCase(runNoun(run))} #${run.id}`;
|
|
17
|
+
// The facts region: a live run's is what the poller swaps; a finished run's stands still.
|
|
18
|
+
const summary = sec("Summary", html `<div id="run-facts">${runFactsRows(run, taskId, running, route)}</div>${running ? html `<p class="k-note" id="run-facts-stamp"></p>` : ""}`);
|
|
19
|
+
// The live transcript (arc 1): the agent's own words, streamed to a file beside the run and polled as raw text.
|
|
20
|
+
// Honesty stated on the surface: display only, and the machine running the agent could alter it.
|
|
21
|
+
const transcript = !running ? "" : sec("What the agent is saying", !peekable
|
|
22
|
+
? note(html `The live transcript is off. Start serve with <span class="k-mono">--runner <name></span> naming this machine's worker, and it appears here.`)
|
|
23
|
+
: run.provider !== "claude"
|
|
24
|
+
? note(`The live transcript needs the claude harness for now; this build runs on ${run.provider}.`)
|
|
25
|
+
: html `${note("Display only: this is not evidence, and the machine running the agent could alter it.")}<pre id="live-transcript" class="k-mono h-pre h-live"></pre><p class="k-note" id="live-transcript-state"></p>`);
|
|
26
|
+
// The live peek region (A2): the poller fills it only on a serve that asserted its runner; otherwise the section
|
|
27
|
+
// says why it is empty, so the click never lands on silence.
|
|
28
|
+
const peek = !running ? "" : sec("What is changing right now", peekable
|
|
29
|
+
? html `<div id="run-peek">${note("Watching… the first look lands within 15 seconds.")}</div><p class="k-note" id="run-peek-stamp"></p>`
|
|
30
|
+
: note(html `The live file view is off. Start serve with <span class="k-mono">--runner <name></span> naming this machine's worker, and it appears here.`));
|
|
31
|
+
const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
|
|
32
|
+
const list = (label, items) => items.length === 0 ? "" : html `<div class="h-list"><p class="k-note">${label}</p><ul>${items.map(one => html `<li>${one}</li>`)}</ul></div>`;
|
|
33
|
+
const handoff = resultSummary === null ? "" : sec("Result", html `<p class="h-read">${resultSummary}</p>${structuredHandoff === null ? "" : [
|
|
34
|
+
list("Completed", structuredHandoff.changes), list("Checks the agent reported", structuredHandoff.verification), list("Follow-up", structuredHandoff.followUps)
|
|
35
|
+
]}`);
|
|
36
|
+
// Review comments on the immutable terminal diff (M6.8): listed, added, and sealed into ONE unapproved revision task.
|
|
37
|
+
// The seal only creates the unapproved task; its approval screen restates the batch.
|
|
38
|
+
const hasTerminalDiff = terminal !== null && terminal.patch !== null && !("problem" in terminal.patch);
|
|
39
|
+
const commentPath = (path, line) => {
|
|
40
|
+
const shown = `${path}${line === null ? "" : `:${line}`}`;
|
|
41
|
+
const href = editor === null ? null : editorFileHref(editor.worktree, path, line);
|
|
42
|
+
return href === null ? html `<span class="k-mono">${shown}</span> ` : html `<a class="k-mono k-link" href="${href}">${shown}</a> `;
|
|
43
|
+
};
|
|
44
|
+
const commentRows = comments.map(one => html `<li class="diff-comment"><span class="k-num k-muted">${whenTime(one.createdAt)}</span><span><b>${one.author}</b> ${one.path === null ? "" : commentPath(one.path, one.line)}${one.note}</span></li>`);
|
|
45
|
+
const commentForm = csrf === "" || !hasTerminalDiff ? null : postForm(`/r/${run.id}/comment`, html `<div class="h-pair">\
|
|
46
|
+
<div class="k-field"><label class="k-label" for="comment-path">File</label><input class="k-input k-mono" id="comment-path" type="text" name="path" placeholder="Choose a line above"></div>\
|
|
47
|
+
<div class="k-field"><label class="k-label" for="comment-line">Line</label><input class="k-input" id="comment-line" type="text" name="line" placeholder="—" inputmode="numeric"></div></div>\
|
|
48
|
+
<div class="k-field"><label class="k-label" for="comment-note">Change requested</label><textarea class="k-textarea" id="comment-note" name="note" rows="2" maxlength="${LIMITS.note}" placeholder="What should change, and why" aria-describedby="comment-note-limit"${noted ? html ` autofocus` : ""}></textarea>\
|
|
49
|
+
<p class="k-hint" id="comment-note-limit">Up to ${LIMITS.note} characters</p></div><div class="k-acts">${btn("Add annotation", { sm: true, type: "submit", attrs: { "data-k-busy": "Adding…" } })}</div>`, { attrs: { class: "h-form", id: "comment-form" } });
|
|
50
|
+
// The device-side half of the editor-link activation (arc 6, finding 1): only when the capability exists and this
|
|
51
|
+
// run belongs to this machine's runner; the person at the browser flips it.
|
|
52
|
+
const editorToggleForm = editorToggle === null || csrf === "" ? "" : postForm("/session/editor-links", html `${btn(editorToggle.on ? "Stop opening files in VS Code here" : "Open files in VS Code from this device", { variant: "ghost", sm: true, type: "submit" })}<p class="k-note">Only useful when this browser runs on the machine that holds the worktrees.</p>`, { attrs: { class: "h-inline" }, returnTo: `/r/${run.id}`, hidden: { on: editorToggle.on ? "0" : "1" } });
|
|
53
|
+
const reviseForm = csrf === "" || comments.length === 0 ? "" : postForm(`/r/${run.id}/revise`, html `${revisionSealFields(comments, sourceDigest)}${slots({
|
|
54
|
+
status: html `<h2>${comments.length} ${comments.length === 1 ? "annotation" : "annotations"} ready</h2>`,
|
|
55
|
+
text: "Creates one revision carrying this exact batch. You review its scope before anything builds.",
|
|
56
|
+
acts: btn("Create revision from annotations", { variant: "primary", type: "submit", attrs: { "data-k-busy": "Creating…" } }),
|
|
57
|
+
terms: "Nothing builds until its scope is approved.",
|
|
58
|
+
})}`, { attrs: { class: "revision-from-comments" } });
|
|
59
|
+
// CI repair, suggestion-first (M8.18): the observed red episode earns a button; the button drafts ONE unapproved
|
|
60
|
+
// task; a person approves it.
|
|
61
|
+
const repair = ciRepair === null || csrf === "" ? "" : sec(`Checks are failing on pull request #${ciRepair.pr}`, html `${note("Seen by the pull request watcher. Drafting a repair makes one unapproved task; you approve its scope before anything builds.")}${postForm(`/r/${run.id}/draft-repair`, btn("Draft a repair task", { sm: true, type: "submit", attrs: { "data-k-busy": "Drafting…" } }), { attrs: { class: "h-inline" } })}`);
|
|
62
|
+
const reviewSec = commentRows.length === 0 && commentForm === null ? "" : sec("Review and revise", html `${note("Annotate the diff above. When the batch is ready, create one scoped revision task from it.")}${commentRows.length === 0 ? "" : html `<ul class="h-notes diff-comments">${commentRows}</ul>`}${commentForm ?? ""}${reviseForm}${editorToggleForm}`);
|
|
63
|
+
const saved = artifacts.length === 0 ? "" : sec("Saved files", html `<ul class="h-files">${artifacts.map(artifact => html `<li><a class="k-link" href="/r/${run.id}/evidence/${artifact.id}">${evidenceWords(artifact.kind)}</a>\
|
|
64
|
+
<span class="k-num k-muted" title="${artifact.bytesStored} bytes stored${artifact.truncated ? ` of ${artifact.bytesOriginal}` : ""}">${artifact.truncated ? `${humanBytes(artifact.bytesStored)} of ${humanBytes(artifact.bytesOriginal)} · partial` : humanBytes(artifact.bytesStored)}</span></li>`)}</ul>`);
|
|
65
|
+
const noteRows = notes.map(one => html `<li><span class="k-num k-muted">${whenTime(one.createdAt)}</span><span><b>${one.author}</b> ${one.note}</span></li>`);
|
|
66
|
+
const noteForm = csrf === "" ? "" : postForm(`/r/${run.id}/note`, html `<label class="k-sr" for="run-note">Add a note</label><input class="k-input" id="run-note" type="text" name="note" placeholder="A note for whoever reads this attempt next" autocomplete="off">${btn("Add note", { sm: true, type: "submit", attrs: { "data-k-busy": "Adding…" } })}`, { attrs: { class: "h-addnote" } });
|
|
67
|
+
const notesSec = notes.length === 0 && noteForm === "" ? "" : sec("Notes", html `${noteRows.length === 0 ? note("No notes yet. A note stays with this attempt for whoever reads it next.") : html `<ul class="h-notes">${noteRows}</ul>`}${noteForm}`);
|
|
68
|
+
// A finished result (package 3) is the ONE shared panel (Summary / Changes / Checks, Request changes beside it), the
|
|
69
|
+
// same markup the review cockpit and the chat's result view render. Every other run keeps its record by record page.
|
|
70
|
+
const resultPanel = result === null ? null : resultPanelHtml(result.detail, {
|
|
71
|
+
place: "run", tab: result.tab, csrf, user: result.user, noted, requestToken: result.requestToken,
|
|
72
|
+
hrefFor: one => (one === "summary" ? `/r/${run.id}` : `/r/${run.id}?tab=${one}`), returnTo: `/r/${run.id}?record=1`, back: null,
|
|
73
|
+
});
|
|
74
|
+
const body = kitPage(html ``, phoneHeader(title, { back: taskHref(taskId) }), doc(docHeader({ title, back: [taskHref(taskId), "Task"], meta: html `Task <a class="k-mono k-link" href="${taskHref(taskId)}">${taskId}</a>${run.startedAt === null ? "" : html ` · started ${whenTime(run.startedAt)}`}` }), summary, transcript, peek, ...(resultPanel === null
|
|
75
|
+
? [handoff, evidenceBundleCard(proofBundle, run, publicationFactsOf(publicationRow), review), terminal === null ? "" : terminalDiffCard(terminal, run.id, editor, commentForm !== null), reviewSec, repair]
|
|
76
|
+
: [html `<div class="h-panel">${resultPanel}</div>`, editorToggleForm]), saved, notesSec));
|
|
77
|
+
return screen(title, body, {
|
|
78
|
+
chrome,
|
|
79
|
+
// One composed functional script (arc 4 contract): the pollers when the run is live, the result panel's script or
|
|
80
|
+
// the comment prefill when a form exists. Neither fetches beyond the pollers.
|
|
81
|
+
...(liveScript === undefined && commentForm === null && resultPanel === null
|
|
82
|
+
? {}
|
|
83
|
+
: { functional: { script: (liveScript ?? "") + (resultPanel !== null ? RESULT_REVIEW_SCRIPT : commentForm === null ? "" : prefillScript()), fetches: liveScript !== undefined } }),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The question an agent asked, on top: what it asked and why, the options as radio cards with its recommendation
|
|
88
|
+
* marked, an optional note, and one Answer (kind none: one click). An irreversible option asks for a tick first; the
|
|
89
|
+
* answer route checks it again. A refusal shows in the box with nothing changed; an answer replaces the box.
|
|
90
|
+
*/
|
|
91
|
+
export function decisionPage(chrome, decision, taskId, artifacts, now, place, returnTo = null, refusal = null,
|
|
92
|
+
/** What a refused answer carried: the choice and the note stay in the box. */
|
|
93
|
+
draft = null) {
|
|
94
|
+
const back = returnTo === null ? ["/", "Home"] : [returnTo, "Back to the chat"];
|
|
95
|
+
const runWords = place.run === null ? null : `${runNoun(place.run)} #${place.run.id}`;
|
|
96
|
+
const project = place.repo === null ? null : projectName(place.repo);
|
|
97
|
+
const meta = html `${project === null ? "" : html `${project} · `}<span class="k-mono">${taskId}</span>${runWords === null ? "" : ` · ${runWords}`} asked ${whenTime(decision.createdAt)}${decision.deadline === null || decision.state !== "open" ? "" : html ` · answer by ${whenTime(decision.deadline)}`}`;
|
|
98
|
+
const open = decision.state !== "answered";
|
|
99
|
+
const irreversible = decision.options.filter(option => !option.reversible);
|
|
100
|
+
let box;
|
|
101
|
+
if (decision.supersededReason !== null) {
|
|
102
|
+
box = slots({ status: html `${chip("Closed")}<h2>${decision.question}</h2>`, text: closedQuestionReason(decision.supersededReason), acts: btn("Open task", { href: taskHref(taskId) }), terms: "Nothing waits on this question now." });
|
|
103
|
+
}
|
|
104
|
+
else if (!open) {
|
|
105
|
+
const chosen = decision.options.find(option => option.id === decision.choice);
|
|
106
|
+
box = slots({
|
|
107
|
+
status: html `${chip("Answered", "done")}<h2>${decision.question}</h2>`,
|
|
108
|
+
text: html `${chosen?.label ?? decision.choice ?? ""} · by ${decision.answeredBy ?? "someone"}${decision.answeredAt === null ? "" : html ` at ${whenTime(decision.answeredAt)}`}${decision.note === null ? "" : ` · “${decision.note}”`}`,
|
|
109
|
+
acts: btn("Open task", { href: taskHref(taskId) }),
|
|
110
|
+
terms: chosen === undefined ? "" : chosen.consequence,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const options = radios({
|
|
115
|
+
name: "choice", legend: "Choose one",
|
|
116
|
+
current: draft?.choice !== null && draft?.choice !== undefined && decision.options.some(option => option.id === draft.choice) ? draft.choice : decision.recommendation,
|
|
117
|
+
items: decision.options.map(option => ({
|
|
118
|
+
value: option.id, label: option.label, text: option.consequence,
|
|
119
|
+
tag: html `${option.id === decision.recommendation ? chip("Recommended", "", { dot: false }) : ""}${option.reversible ? "" : chip("Can't be undone", "failed", { dot: false })}`,
|
|
120
|
+
})),
|
|
121
|
+
});
|
|
122
|
+
const noteField = html `<div class="k-field"><label class="k-label" for="answer-note">Note for the agent <span class="k-muted">(optional)</span></label><textarea class="k-textarea" id="answer-note" name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Anything it should know with your answer">${draft?.note ?? ""}</textarea></div>`;
|
|
123
|
+
const confirm = irreversible.length === 0 ? "" : html `<label class="h-check"><input type="checkbox" name="confirm" value="yes"><span>I understand ${irreversible.length === 1 ? `“${irreversible[0].label}” can't be undone` : "the options marked so can't be undone"}.</span></label>`;
|
|
124
|
+
const overdue = isOverdue(decision, now);
|
|
125
|
+
box = postForm(`/d/${decision.id}/answer`, slots({
|
|
126
|
+
wait: true,
|
|
127
|
+
status: html `${status("needs")}<h2>${decision.question}</h2>`,
|
|
128
|
+
text: decision.recap,
|
|
129
|
+
body: html `${options}${noteField}${confirm}`,
|
|
130
|
+
acts: btn("Answer", { variant: "primary", type: "submit", docked: true, attrs: { "data-k-busy": "Answering…" } }),
|
|
131
|
+
terms: html `Answering needs no password.${overdue ? " The answer was due; the build still waits for it." : ""}`,
|
|
132
|
+
err: refusal === null ? undefined : `${sentenceCase(refusal)}. Nothing changed.`,
|
|
133
|
+
}), { attrs: { class: "h-question", id: "answer-form" }, returnTo });
|
|
134
|
+
}
|
|
135
|
+
const looked = artifacts.length === 0 ? "" : sec("What the agent looked at", html `<ul class="h-files">${artifacts.map(artifact => html `<li><a class="k-link" href="/d/${decision.id}/evidence/${artifact.id}">${evidenceWords(artifact.kind)}</a><span class="k-num k-muted">${humanBytes(artifact.bytesStored)}${artifact.truncated ? " · partial" : ""}</span></li>`)}</ul>`);
|
|
136
|
+
const stopped = !open || place.run === null ? "" : sec("Where it stopped", html `<p class="h-read k-muted">${sentenceCase(runWords ?? "")} asked this and holds its work until you answer. <a class="k-link" href="/r/${place.run.id}?record=1">See the attempt</a></p>`);
|
|
137
|
+
const answering = open && decision.supersededReason === null;
|
|
138
|
+
const body = kitPage(html ``, phoneHeader(project ?? "Question", { back: back[0] }),
|
|
139
|
+
// The shell's phone header already says this page's title (the screen title), so the h1 is drawn once.
|
|
140
|
+
doc(docHeader({ title: place.title ?? taskId, back, meta, same: true }), box, looked, stopped),
|
|
141
|
+
// On a phone the one primary lives in the dock; it submits the same form.
|
|
142
|
+
answering ? dock(btn("Answer", { variant: "primary", type: "submit", attrs: { form: "answer-form", "data-k-busy": "Answering…" } })) : "");
|
|
143
|
+
return screen(place.title ?? `Question · ${taskId}`, body, { chrome });
|
|
144
|
+
}
|
|
145
|
+
// ---- /t/<id>/evidence: the full record ----------------------------------------------------------------------------
|
|
146
|
+
const at = (iso) => iso === null ? "—" : whenTime(iso);
|
|
147
|
+
const shortHash = (hash) => hash === null ? "—" : html `<span class="k-mono" title="${hash}">${hash.slice(0, 12)}</span>`;
|
|
148
|
+
const usd = (value) => value === null ? "—" : `$${value.toFixed(value < 1 && value > 0 ? 4 : 2)}`;
|
|
149
|
+
const webLink = (url) => /^https:\/\/[^\s"'<>]+$/.test(url) ? html `<a class="k-link" href="${url}">${url}</a>` : html `${url}`;
|
|
150
|
+
const filerWords = (who) => who === null || who.name === null ? "automation" : who.kind === "person" ? who.name : `${who.name} (${who.kind})`;
|
|
151
|
+
const basisWords = {
|
|
152
|
+
person: "", mode: " (operating mode)", flow: " (the flow runs on its own)", lead: " (the lead, not a person)", schedule: " (standing order)",
|
|
153
|
+
carried: " (carried from their release approval)", unknown: " (no person's proof on record)",
|
|
154
|
+
};
|
|
155
|
+
function stateChip(state) {
|
|
156
|
+
return state === "done" ? status("finished") : state === "failed" ? status("failed") : state === "running" ? status("running")
|
|
157
|
+
: state === "queued" ? status("queued") : chip(sentenceCase(state));
|
|
158
|
+
}
|
|
159
|
+
/** A task's full record, read-only: what was asked and approved, every agent that ran, the result, and its sealed audit
|
|
160
|
+
* log entries, with the JSON download and Print. The same facts as the pack's JSON (evidence-pack.ts). */
|
|
161
|
+
export function evidenceRecordPage(chrome, pack) {
|
|
162
|
+
const task = pack.task;
|
|
163
|
+
const href = `/t/${encodeURIComponent(task.id)}`;
|
|
164
|
+
const chain = pack.ledger.chain;
|
|
165
|
+
const chainWords = chain.ok
|
|
166
|
+
? html `<span data-evidence-chain="ok">Chain verified · ${chain.entries} entries · head ${shortHash(chain.head)}${pack.ledger.checkpoint === null ? "" : ` · last checkpoint at entry #${pack.ledger.checkpoint.through}`}</span>`
|
|
167
|
+
: html `<span data-evidence-chain="broken">Chain broken: ${chain.problem ?? "it doesn't verify"}</span>`;
|
|
168
|
+
const rules = pack.rules === null ? "None: every plan asks a person" : html `${sentenceCase(pack.rules.now)}${pack.rules.changes.length === 0 ? "" : disclose(`${pack.rules.changes.length} ${pack.rules.changes.length === 1 ? "change" : "changes"}`, html `<ul class="h-notes">${pack.rules.changes.slice(-5).map(one => html `<li><span class="k-num k-muted">${at(one.at)}</span><span>${one.by}: ${one.change ?? ""}</span></li>`)}</ul>`)}`;
|
|
169
|
+
const summary = sec("Summary", facts([
|
|
170
|
+
["Task", html `<span class="k-mono">${task.id}</span>${task.project === null ? "" : ` · ${task.project}`}`],
|
|
171
|
+
["Status", stateChip(task.state)],
|
|
172
|
+
["Approval rules", rules],
|
|
173
|
+
["Agents", pack.totals.runs === 0 ? "None ran" : `${pack.totals.runs} ${pack.totals.runs === 1 ? "run" : "runs"} · ${usd(pack.totals.costUsd)}`],
|
|
174
|
+
["Prepared", html `${at(pack.generatedAt)} for ${pack.generatedBy}`],
|
|
175
|
+
["Audit log", chainWords],
|
|
176
|
+
]));
|
|
177
|
+
const broken = chain.ok ? "" : notice(html `The audit log's chain doesn't verify: ${chain.problem ?? "an entry changed"}. The record below is what the log holds now.`);
|
|
178
|
+
const versions = pack.versions.map((version, index) => {
|
|
179
|
+
const scope = version.scope;
|
|
180
|
+
const approved = scope?.approved ?? null;
|
|
181
|
+
const approvals = scope === null ? [] : scope.approvals.filter(one => one.digest === (approved?.digest ?? scope.digest));
|
|
182
|
+
const rows = [["Filed", html `${at(version.filedAt)} by ${filerWords(version.filedBy)}`]];
|
|
183
|
+
if (scope === null)
|
|
184
|
+
rows.push(["Plan", "None written"]);
|
|
185
|
+
else {
|
|
186
|
+
rows.push(["Goal", scope.goal]);
|
|
187
|
+
if (scope.outOfScope)
|
|
188
|
+
rows.push(["Not in scope", scope.outOfScope]);
|
|
189
|
+
if (scope.touches.length)
|
|
190
|
+
rows.push(["Touches", joinHtml(scope.touches.map(one => html `<span class="k-mono">${one}</span>`), ", ")]);
|
|
191
|
+
if (scope.acceptance.length)
|
|
192
|
+
rows.push(["Requirements", html `<ol class="h-ol">${scope.acceptance.map(one => html `<li>${one.statement}</li>`)}</ol>`]);
|
|
193
|
+
if (scope.budgetUsd !== null)
|
|
194
|
+
rows.push(["Budget", usd(scope.budgetUsd)]);
|
|
195
|
+
if (scope.writtenBy.length)
|
|
196
|
+
rows.push(["Plan written by", [...new Set(scope.writtenBy.map(one => one.author))].join(", ")]);
|
|
197
|
+
rows.push(["Approved", approved === null ? "Not approved" : html `${approved.by}${basisWords[approved.basis]} · ${at(approved.at)}${approved.current ? "" : " · the plan changed after this"}`]);
|
|
198
|
+
if (approvals.length > 1)
|
|
199
|
+
rows.push(["Approvals", joinHtml(approvals.map(one => html `${one.approver} · ${at(one.at)}`), html `<br>`)]);
|
|
200
|
+
rows.push(["Plan version", shortHash(scope.digest)]);
|
|
201
|
+
}
|
|
202
|
+
const heading = pack.versions.length === 1 ? "Request and approval" : index === 0 ? "Request and approval" : `Revision ${index}`;
|
|
203
|
+
return sec(heading, html `${pack.versions.length === 1 ? "" : html `<p class="k-note k-mono">${version.id}</p>`}${facts(rows)}`, html ` data-evidence-version="${version.id}"`);
|
|
204
|
+
});
|
|
205
|
+
const runs = pack.versions.flatMap(version => version.runs);
|
|
206
|
+
const agents = sec("Agents", runs.length === 0 ? note("No agent has run for this task yet.") : html `<div class="h-table" role="table" aria-label="Agents">\
|
|
207
|
+
<div class="h-tr h-th" role="row"><span role="columnheader">Run</span><span role="columnheader">Role</span><span role="columnheader">Agent</span><span role="columnheader">Outcome</span><span role="columnheader">Spend</span><span role="columnheader">Finished</span><span role="columnheader">Commit</span></div>\
|
|
208
|
+
${runs.map(run => html `<div class="h-tr" role="row"><span role="cell"><a class="k-mono k-link" href="/r/${run.id}?record=1">#${run.id}</a></span><span role="cell">${run.role}</span>\
|
|
209
|
+
<span role="cell">${run.agent}${run.model ? html ` <span class="k-mono">${run.model}</span>` : ""}</span><span role="cell">${run.outcome === null ? "Running" : sentenceCase(run.outcome)}</span>\
|
|
210
|
+
<span role="cell" class="k-num">${usd(run.costUsd)}</span><span role="cell">${at(run.finishedAt)}</span><span role="cell">${shortHash(run.head)}</span></div>`)}</div>${runs.some(run => run.role === "builder" || run.role === "repair") ? disclose("Changed files", html `<div class="h-changes">${runs.filter(run => run.role === "builder" || run.role === "repair").map(run => html `<p class="k-note">Run #${run.id}${run.diff === null ? "" : html ` · diff ${shortHash(run.diff.sha256)}${run.diff.complete ? "" : " (partial)"}`}</p>${run.changedFiles === null ? note("Its changed files couldn't be read.") : run.changedFiles.length === 0 ? note("No files changed.") : html `<ul class="h-files">${run.changedFiles.map(file => html `<li><span class="k-mono">${file}</span></li>`)}</ul>`}`)}</div>`) : ""}`);
|
|
211
|
+
const result = pack.result;
|
|
212
|
+
const outcome = sec("Result", result === null ? note("No result yet.") : facts([
|
|
213
|
+
["Checks", html `${result.checks.detail}${result.checks.command ? html ` <span class="k-mono">${result.checks.command}</span>` : ""}`],
|
|
214
|
+
["Completed", result.completedBy === null ? "Not yet" : html `${result.completedBy} · ${at(result.completedAt)}`],
|
|
215
|
+
["Published", result.publication === null ? "Not published" : html `${sentenceCase(result.publication.state)}${result.publication.prUrl ? html ` · ${webLink(result.publication.prUrl)}` : ""}${result.publication.remoteState ? ` · ${result.publication.remoteState.toLowerCase()}` : ""}`],
|
|
216
|
+
["Commit", html `${shortHash(result.base)} → ${shortHash(result.head)}`],
|
|
217
|
+
...(result.caveats.length ? [["Caveats", joinHtml(result.caveats, html `<br>`)]] : []),
|
|
218
|
+
]), html ` data-evidence-result`);
|
|
219
|
+
const entries = pack.ledger.entries.filter(one => one.source !== "request");
|
|
220
|
+
const requests = pack.ledger.entries.length - entries.length;
|
|
221
|
+
const log = sec("Audit log", html `${entries.length === 0 ? note("No entries.") : html `<ul class="h-log">${[...entries].reverse().map(one => html `<li data-ledger-id="${one.id}">\
|
|
222
|
+
<span class="k-num k-muted">${at(one.at)}</span><span><b>${one.actor}</b> ${one.action}${one.detail ? html `<span class="k-muted"> · ${one.detail}</span>` : ""}</span>\
|
|
223
|
+
<span class="k-muted">${one.outcome}</span><span class="h-seal">${shortHash(one.seal?.hash ?? null)}</span></li>`)}</ul>`}${requests > 0 ? note(`Plus ${requests} ${requests === 1 ? "request" : "requests"} made through the console (accepted or refused), listed in the JSON.`) : ""}${pack.ledger.truncated ? note(`This record holds ${entries.length} of the task's entries and the newest ${requests} requests; the audit log export has the rest.`) : ""}`);
|
|
224
|
+
const json = `${href}/evidence?format=json`;
|
|
225
|
+
const tools = html `<span class="k-acts h-tools">${btn("Download JSON", { sm: true, href: json, attrs: { download: true } })}${btn("Print", { sm: true, variant: "ghost", attrs: { "data-k-print": true } })}</span>`;
|
|
226
|
+
const body = kitPage(html ` data-evidence-pack="${task.id}"`, phoneHeader("Full record", { back: href }), doc(docHeader({ title: "Full record", back: [href, "Task"], meta: html `${task.title}${task.project === null ? "" : ` · ${task.project}`} · <span class="k-mono">${task.id}</span>`, right: tools }), html `<p class="k-note h-lead">Read-only. Everything below is sealed in the audit log; nothing here can be changed.</p>`, broken, summary, versions, agents, outcome, log, html `<p class="k-note h-foot">Record digest <span class="k-mono">${pack.digest}</span>. Each entry's seal can be rechecked from the JSON: <span class="k-mono">${pack.ledger.recipe}</span>.</p>`, html `<div class="h-phone-tools">${btn("Download JSON", { href: json, attrs: { download: true } })}</div>`));
|
|
227
|
+
return screen(`Full record · ${task.title}`, body, { chrome });
|
|
228
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { QualityMode } from "../quality.js";
|
|
2
|
+
import type { UnattendedPermissionMode } from "../scope.js";
|
|
3
|
+
import { type Chrome, type Screen } from "./chrome.js";
|
|
4
|
+
export type TaskComposerPrefill = {
|
|
5
|
+
title: string;
|
|
6
|
+
goal: string;
|
|
7
|
+
not: string;
|
|
8
|
+
touches: string;
|
|
9
|
+
acceptance: string;
|
|
10
|
+
values?: URLSearchParams;
|
|
11
|
+
};
|
|
12
|
+
export declare function newTaskPage(chrome: Chrome, project: string | null,
|
|
13
|
+
/** undefined: keep the revision the refused form posted (its draft), so a project switch since still refuses. */
|
|
14
|
+
projectRevision: number | undefined, problem: string | null, candidates?: {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
}[], permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, projects?: {
|
|
18
|
+
path: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}[],
|
|
21
|
+
/** A shipped template's fields, or a refused filing's own words, back in the form. */
|
|
22
|
+
prefill?: TaskComposerPrefill | null,
|
|
23
|
+
/** Words an idea's link put in the box (GET ?text=). */
|
|
24
|
+
draft?: string | null,
|
|
25
|
+
/** Ideas for the chosen project: one tap drafts the words into the box. */
|
|
26
|
+
suggestions?: ReadonlyArray<{
|
|
27
|
+
label: string;
|
|
28
|
+
draft: string;
|
|
29
|
+
}>): Screen;
|