toolroll 0.9.72 → 0.9.74
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 +108 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-actions.js +13 -8
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/chat-quiet.js +3 -3
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +20 -19
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +84 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +11 -2
- package/dist/release.js +82 -6
- package/dist/repository-context-ui.js +2 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +34 -23
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/flows.js +40 -10
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +29 -8
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +34 -1
- package/dist/store.js +81 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/subscription-chat.js +5 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
package/dist/server/live.js
CHANGED
|
@@ -9,9 +9,11 @@ import { handlersOf } from "./handler-registry.js";
|
|
|
9
9
|
import { respond } from "./http.js";
|
|
10
10
|
import { adapterPolicy } from "./route-policy.js";
|
|
11
11
|
import { requestContext } from "./request-context.js";
|
|
12
|
+
import { createAgentsLiveRooms } from "./agents-live-room.js";
|
|
13
|
+
import { leadTilesFor } from "./agents-live-lead.js";
|
|
12
14
|
/** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
|
|
13
15
|
export const LIVE_ROOMS_PER_PAGE = 6;
|
|
14
|
-
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
16
|
+
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent|summary|agents-live)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
15
17
|
/** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
|
|
16
18
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
17
19
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
@@ -34,6 +36,7 @@ export function parseLiveRoom(name) {
|
|
|
34
36
|
switch (kind) {
|
|
35
37
|
case "workspace": return key === null && query === undefined ? { kind } : null;
|
|
36
38
|
case "summary": return key === null && query === undefined ? { kind } : null;
|
|
39
|
+
case "agents-live": return key === null && query === undefined ? { kind } : null;
|
|
37
40
|
case "task": return key !== null && key !== "" && query === undefined ? { kind, task: key } : null;
|
|
38
41
|
case "flow": {
|
|
39
42
|
const flow = Number(key), card = params.get("card");
|
|
@@ -92,6 +95,11 @@ export function createLiveHandlers(runtime, live) {
|
|
|
92
95
|
const chatViewers = new Set();
|
|
93
96
|
// One agent's open view (Agents board): what it shows, keyed by the exact run.
|
|
94
97
|
const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
|
|
98
|
+
// The Live grid: one room per viewer and scope, watching only (no lease, no seen mark).
|
|
99
|
+
// Each Live page's own lead tiles, read as its viewer's saved session now (a page's room key carries the page).
|
|
100
|
+
const liveLeads = new Map();
|
|
101
|
+
const agentsLive = createAgentsLiveRooms({ store, bus: live.bus, evidenceRoot: runtime.evidenceRoot, localRunner: runtime.options.localRunner, clock, visible,
|
|
102
|
+
leads: key => liveLeads.get(key)?.() ?? null, forget: key => liveLeads.delete(key) });
|
|
95
103
|
const readBrowser = (request, who) => rereadBrowser(runtime, request, who);
|
|
96
104
|
const sameBrowser = (request, who) => () => readBrowser(request, who) !== null;
|
|
97
105
|
function admitted(viewer, thread) {
|
|
@@ -196,10 +204,11 @@ export function createLiveHandlers(runtime, live) {
|
|
|
196
204
|
if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
|
|
197
205
|
return json(response, 400, { error: "room" });
|
|
198
206
|
const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
|
|
199
|
-
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1)
|
|
207
|
+
if (rooms.some(one => one.room === null) || rooms.filter(one => one.room?.kind === "agent").length > 1 || rooms.filter(one => one.room?.kind === "workspace").length > 1 || rooms.filter(one => one.room?.kind === "chat").length > 1 || rooms.filter(one => one.room?.kind === "team").length > 1 || rooms.filter(one => one.room?.kind === "summary").length > 1 || rooms.filter(one => one.room?.kind === "agents-live").length > 1)
|
|
200
208
|
return json(response, 400, { error: "room" });
|
|
201
209
|
// Admit every room before the stream opens, with the answers each page's own stream gave.
|
|
202
210
|
const joins = [];
|
|
211
|
+
let refusedJoin = false;
|
|
203
212
|
for (const { name, room } of rooms) {
|
|
204
213
|
if (room.kind !== "team" && who.via !== "cookie")
|
|
205
214
|
return json(response, 403, { error: "session" });
|
|
@@ -216,6 +225,33 @@ export function createLiveHandlers(runtime, live) {
|
|
|
216
225
|
joins.push(connection => summaryRooms.join(summaryKey(who.name, scope), { name: who.name, room: name, connection,
|
|
217
226
|
valid: () => signedIn() && live.projectScopeFor(who.name) === scope }));
|
|
218
227
|
}
|
|
228
|
+
else if (room.kind === "agents-live") {
|
|
229
|
+
const browser = who;
|
|
230
|
+
const scope = live.projectScopeFor(who.name);
|
|
231
|
+
if (scope === null)
|
|
232
|
+
return json(response, 404, { error: "agents-live" });
|
|
233
|
+
const signedIn = sameBrowser(request, browser);
|
|
234
|
+
const valid = () => signedIn() && live.projectScopeFor(who.name) === scope;
|
|
235
|
+
joins.push(connection => {
|
|
236
|
+
// Proved before the room is made or read: an invalid viewer never causes a look at a file.
|
|
237
|
+
let ok = false;
|
|
238
|
+
try {
|
|
239
|
+
ok = valid();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
ok = false;
|
|
243
|
+
}
|
|
244
|
+
if (!ok) {
|
|
245
|
+
connection.send(name, "gone");
|
|
246
|
+
refusedJoin = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Each page's own room (its key carries this connection): a newcomer is never handed a digest computed earlier.
|
|
250
|
+
const key = agentsLive.keyOf(who.name, scope, randomBytes(9).toString("hex"));
|
|
251
|
+
liveLeads.set(key, () => { const again = readBrowser(request, browser); return again === null ? null : leadTilesFor(runtime, again); });
|
|
252
|
+
agentsLive.rooms.join(key, { name: who.name, room: name, connection, valid });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
219
255
|
else if (room.kind === "task") {
|
|
220
256
|
const browser = who;
|
|
221
257
|
const family = familyOf(room.task);
|
|
@@ -342,6 +378,9 @@ export function createLiveHandlers(runtime, live) {
|
|
|
342
378
|
for (const join of joins)
|
|
343
379
|
if (connection.open)
|
|
344
380
|
join(connection);
|
|
381
|
+
// A page whose only room was refused at its join ends now (`ended`), its keep-alive stopped, not left open.
|
|
382
|
+
if (refusedJoin && connection.rooms === 0)
|
|
383
|
+
connection.refuse(() => connection.close());
|
|
345
384
|
request.once("close", () => connection.close());
|
|
346
385
|
}
|
|
347
386
|
const detachChat = live.bus.subscribe(() => {
|
|
@@ -357,6 +396,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
357
396
|
summaryRooms.close();
|
|
358
397
|
teamRooms.close();
|
|
359
398
|
agentRooms.close();
|
|
399
|
+
agentsLive.rooms.close();
|
|
360
400
|
for (const viewer of [...chatViewers])
|
|
361
401
|
viewer.connection.close();
|
|
362
402
|
},
|
package/dist/server/pages.js
CHANGED
|
@@ -26,6 +26,10 @@ import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
|
26
26
|
import { ledgerBody } from "../ledger-view.js";
|
|
27
27
|
import { agentLanesOf, agentRowOf, agentsBoardOf, agentFactsFor, laneWindowOf, sealedDiffOf } from "../agents-board.js";
|
|
28
28
|
import { markAgentSeen } from "../agent-board.js";
|
|
29
|
+
import { agentsLiveOf } from "../agents-live.js";
|
|
30
|
+
import { readChildWindow } from "../live-children-read.js";
|
|
31
|
+
import { readMainWindow } from "../live-main-read.js";
|
|
32
|
+
import { runLiveCommands } from "../store.js";
|
|
29
33
|
import { prometheusMetrics } from "../metrics.js";
|
|
30
34
|
import { readMonitoring } from "../monitoring-settings.js";
|
|
31
35
|
import { targetOf } from "../monitoring.js";
|
|
@@ -38,6 +42,7 @@ import { healthWords } from "../server-telemetry.js";
|
|
|
38
42
|
import { tally } from "../summary.js";
|
|
39
43
|
import { serveInstallAsset } from "./pages-assets.js";
|
|
40
44
|
import { healthz } from "./remote-hooks.js";
|
|
45
|
+
import { leadsStillAdmitted, leadTilesFor } from './agents-live-lead.js';
|
|
41
46
|
import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
|
|
42
47
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
43
48
|
import { assignmentOf } from "../assignment.js";
|
|
@@ -46,6 +51,7 @@ import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage,
|
|
|
46
51
|
import { capsPage } from "./render-settings.js";
|
|
47
52
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
48
53
|
import {} from "./session.js";
|
|
54
|
+
import { rereadBrowser } from "./live.js";
|
|
49
55
|
export function createPagesHandlers(runtime) {
|
|
50
56
|
const { clock, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
51
57
|
// The filesystem browser (operator request): pick a project folder by
|
|
@@ -444,8 +450,14 @@ export function createPagesHandlers(runtime) {
|
|
|
444
450
|
const now = clock();
|
|
445
451
|
const access = workAccess();
|
|
446
452
|
const asked = url.searchParams.get("run");
|
|
453
|
+
// The Live grid only watches (docs/plans/agents-view.md §3): it branches off before an opened agent is built, so
|
|
454
|
+
// viewing it never marks a run seen, takes a watch or offers a composer — a reload of ?view=live&run=… included.
|
|
455
|
+
const watching = url.searchParams.get("view") === "live";
|
|
456
|
+
// Live is a browser session's only, like its reads (no token sees the grid).
|
|
457
|
+
if (watching && who.via !== "cookie")
|
|
458
|
+
return respond(response, 403, "text/plain; charset=utf-8", "The Live view is for a signed-in browser.");
|
|
447
459
|
let selected = null;
|
|
448
|
-
if (asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
460
|
+
if (!watching && asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
449
461
|
const run = store.getRun(Number(asked));
|
|
450
462
|
const facts = run === null || !runVisible(run) ? null : agentFactsFor(store, run.id, access, who.name);
|
|
451
463
|
if (facts !== null) {
|
|
@@ -485,6 +497,11 @@ export function createPagesHandlers(runtime) {
|
|
|
485
497
|
};
|
|
486
498
|
}
|
|
487
499
|
}
|
|
500
|
+
// The Live grid is drawn in the browser from its own read-only reads (agents-live-view.tsx): no board facts here.
|
|
501
|
+
if (watching) {
|
|
502
|
+
const view = { kind: "agents", groups: [], selected: null, lanes: null, empty: null, live: true };
|
|
503
|
+
return sendScreen(response, 200, screen("Agents", html `<h1>Agents</h1><p class="meta">Live needs JavaScript.</p>`, { chrome: chromeFor(project, "runs"), workspace: { view } }));
|
|
504
|
+
}
|
|
488
505
|
const groups = agentsBoardOf(store, now, access, who.name, evidenceRoot);
|
|
489
506
|
const lanes = selected === null ? agentLanesOf(store, now, access, who.name, laneWindowOf(url.searchParams.get("window"))) : null;
|
|
490
507
|
const view = { kind: "agents", groups, selected, lanes,
|
|
@@ -989,6 +1006,67 @@ export function createPagesHandlers(runtime) {
|
|
|
989
1006
|
response.setHeader("cache-control", "no-store");
|
|
990
1007
|
return respond(response, 200, "application/json", JSON.stringify({ proof: createHmac("sha256", Buffer.from(identity, "hex")).update(challenge).digest("hex") }));
|
|
991
1008
|
}
|
|
1009
|
+
// The Live grid (docs/plans/agents-view.md §3), kept after every other handler.
|
|
1010
|
+
/** The Live grid's tiles (JSON, read-only): every role's running agents and those that just ended, that this viewer
|
|
1011
|
+
* may see — the console's ceiling and the account's own access, proved per run, on every request. */
|
|
1012
|
+
async function agentsLive(ctx) {
|
|
1013
|
+
const { who, request, response } = ctx;
|
|
1014
|
+
response.setHeader("cache-control", "no-store");
|
|
1015
|
+
// A browser session only (the route says so too): live output is never a token's to read.
|
|
1016
|
+
if (who.via !== "cookie")
|
|
1017
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
1018
|
+
const browser = who;
|
|
1019
|
+
const canSee = (run) => { const repo = taskRepoOf(run.taskRef); return repo !== null && visible(repo) && store.accountCanAccess(who.name, repo); };
|
|
1020
|
+
const tiles = agentsLiveOf(store, clock(), { canSee, localRunner: options.localRunner });
|
|
1021
|
+
// The viewer's own lead turns, admitted as their chat is, from the session as it is saved now.
|
|
1022
|
+
const current = rereadBrowser(runtime, request, browser);
|
|
1023
|
+
const leads = current === null ? [] : leadTilesFor(runtime, current);
|
|
1024
|
+
// Re-proved right before delivery: the session still held, each tile's run still in view from its current row, and
|
|
1025
|
+
// each lead turn still admitted, from the session as it is saved now.
|
|
1026
|
+
const again = current === null ? null : rereadBrowser(runtime, request, browser);
|
|
1027
|
+
if (again === null)
|
|
1028
|
+
return respond(response, 401, "application/json", JSON.stringify({ error: "session" }));
|
|
1029
|
+
const shown = tiles.filter(tile => { const run = store.getRun(tile.runId); return run !== null && canSee(run); });
|
|
1030
|
+
return respond(response, 200, "application/json", JSON.stringify({ tiles: shown, leads: leadsStillAdmitted(runtime, again, leads) }));
|
|
1031
|
+
}
|
|
1032
|
+
/** This console's key for the Live grid's read cursors: random, in memory only (never where a build can reach), so a
|
|
1033
|
+
* restart makes old cursors start over visibly. A cursor is state, never permission. */
|
|
1034
|
+
const liveCursorKey = randomBytes(32);
|
|
1035
|
+
/** One window of a run's live output for the Live grid (JSON, read-only): its own records or its subagents'. Access
|
|
1036
|
+
* is proved before the read (ceiling, account, this computer's run) and again after it, before a byte is sent. */
|
|
1037
|
+
async function agentsLiveRead(ctx) {
|
|
1038
|
+
const { url, who, response } = ctx;
|
|
1039
|
+
response.setHeader("cache-control", "no-store");
|
|
1040
|
+
// A browser session only (the route says so too): live output is never a token's to read.
|
|
1041
|
+
if (who.via !== "cookie")
|
|
1042
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
1043
|
+
const runId = Number(url.pathname.slice("/agents/live/".length));
|
|
1044
|
+
const file = url.searchParams.get("file");
|
|
1045
|
+
const cursor = url.searchParams.get("cursor");
|
|
1046
|
+
if ((file !== "main" && file !== "sub") || [...url.searchParams.keys()].some(key => key !== "file" && key !== "cursor")) {
|
|
1047
|
+
return respond(response, 400, "application/json", JSON.stringify({ error: "request" }));
|
|
1048
|
+
}
|
|
1049
|
+
const browser = who;
|
|
1050
|
+
// The saved session still held (not just the cookie), and the run still in view from its current row.
|
|
1051
|
+
const allowed = () => {
|
|
1052
|
+
if (rereadBrowser(runtime, ctx.request, browser) === null)
|
|
1053
|
+
return false;
|
|
1054
|
+
const run = store.getRun(runId);
|
|
1055
|
+
const repo = run === null ? null : taskRepoOf(run.taskRef);
|
|
1056
|
+
return run !== null && repo !== null && runVisible(run) && store.accountCanAccess(who.name, repo);
|
|
1057
|
+
};
|
|
1058
|
+
if (!allowed())
|
|
1059
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1060
|
+
if (options.localRunner === undefined || store.getRun(runId)?.runner !== options.localRunner) {
|
|
1061
|
+
return allowed() ? respond(response, 200, "application/json", JSON.stringify({ error: "off" })) : respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1062
|
+
}
|
|
1063
|
+
const read = { key: liveCursorKey, commands: runLiveCommands(store, runId) };
|
|
1064
|
+
const window = file === "sub" ? readChildWindow(evidenceRoot, runId, cursor, read) : readMainWindow(evidenceRoot, runId, cursor, read);
|
|
1065
|
+
// Re-proved after the read: a narrowed account or a run gone from view gets nothing it read.
|
|
1066
|
+
if (!allowed() || store.getRun(runId)?.runner !== options.localRunner)
|
|
1067
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1068
|
+
return respond(response, 200, "application/json", JSON.stringify(window.ok ? window : { error: window.reason }));
|
|
1069
|
+
}
|
|
992
1070
|
const registrations = handlersOf("pages", {
|
|
993
1071
|
"projects.browse": projectsBrowse,
|
|
994
1072
|
"projects.github": projectsGithub,
|
|
@@ -1001,6 +1079,8 @@ export function createPagesHandlers(runtime) {
|
|
|
1001
1079
|
"system": systemScreen,
|
|
1002
1080
|
"peek": peekPage,
|
|
1003
1081
|
"agents": agentsPage,
|
|
1082
|
+
"agents.live": agentsLive,
|
|
1083
|
+
"agents.live.read": agentsLiveRead,
|
|
1004
1084
|
"fleet": fleetPage,
|
|
1005
1085
|
"ledger.export": ledgerExport,
|
|
1006
1086
|
"menu": menuPage,
|
|
@@ -359,8 +359,14 @@ export declare function leadAfterComposerHtml(data: {
|
|
|
359
359
|
pending: LeadTurn | null;
|
|
360
360
|
focusTask: TaskChatFocus | null;
|
|
361
361
|
}): Html;
|
|
362
|
+
/** A conversation shown before it exists (quiet page visits): its page binds sends to this session id, and the first
|
|
363
|
+
* send starts the conversation (chatSend). A visit never starts one. */
|
|
364
|
+
export declare const NOT_STARTED_SESSION = 0;
|
|
362
365
|
export declare function leadPage(chrome: Chrome, data: LeadThreadRows & {
|
|
363
|
-
|
|
366
|
+
/** Null: no conversation yet; the first message starts one. */
|
|
367
|
+
session: LeadSession | null;
|
|
368
|
+
/** Whose conversation this is (the session's approver once it exists). */
|
|
369
|
+
approver: string;
|
|
364
370
|
/** A project's own thread (v77); null for the lead conversation or a task. */
|
|
365
371
|
chatProject?: string | null;
|
|
366
372
|
follow?: {
|
|
@@ -390,6 +396,14 @@ export type LiveTurn = {
|
|
|
390
396
|
ok: boolean;
|
|
391
397
|
listeners: Set<() => void>;
|
|
392
398
|
expiry?: NodeJS.Timeout;
|
|
399
|
+
/** For the Live grid's lead tile (agents-live-lead.ts): the turn's id once it says it, when it started and ended, and
|
|
400
|
+
* when each step began (epoch ms). Never sent to the chat room. */
|
|
401
|
+
turn?: number;
|
|
402
|
+
startedAt?: number;
|
|
403
|
+
endedAt?: number;
|
|
404
|
+
stepAt?: number[];
|
|
405
|
+
/** Per step: its text is the provider's finished answer, secret-checked whole (lead.ts `final`). */
|
|
406
|
+
confirmed?: boolean[];
|
|
393
407
|
};
|
|
394
408
|
export type ChatEnablement = {
|
|
395
409
|
ok: true;
|
|
@@ -952,18 +952,25 @@ export function leadThreadHtml(data) {
|
|
|
952
952
|
export function leadAfterComposerHtml(data) {
|
|
953
953
|
return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? leadPromptStarters(data.focusTask) : ""}</div>`;
|
|
954
954
|
}
|
|
955
|
+
/** A conversation shown before it exists (quiet page visits): its page binds sends to this session id, and the first
|
|
956
|
+
* send starts the conversation (chatSend). A visit never starts one. */
|
|
957
|
+
export const NOT_STARTED_SESSION = 0;
|
|
955
958
|
export function leadPage(chrome, data) {
|
|
956
959
|
const subscription = isSubscriptionChatProvider(data.config.provider);
|
|
957
960
|
const chatProject = data.focusTask === null ? data.chatProject ?? null : null;
|
|
958
961
|
const returnTo = data.focusTask !== null ? taskChatHref(data.focusTask.id) : chatProject !== null ? projectChatHref(chatProject) : "/chat";
|
|
962
|
+
const sessionId = data.session?.id ?? NOT_STARTED_SESSION;
|
|
963
|
+
const approver = data.session?.approver ?? data.approver;
|
|
959
964
|
const controlsHtml = joinHtml([
|
|
960
|
-
html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
|
|
965
|
+
data.session === null ? "" : html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
|
|
961
966
|
html `<details class="chat-limits chat-session-details"><summary>Conversation details<span class="meta">${subscription ? "membership" : data.config.provider}</span></summary>\n${joinHtml([
|
|
962
967
|
html `<div class="chat-budget"><span class="mono">${data.config.provider} · ${data.config.model}</span><span>${data.turnsToday} / ${data.config.dailyTurns} turns today</span>${subscription
|
|
963
968
|
? html `<span>membership login · no dollar ceiling</span>`
|
|
964
|
-
: html `<span>this conversation: ${chatMoney(data.session
|
|
965
|
-
|
|
966
|
-
|
|
969
|
+
: html `<span>this conversation: ${chatMoney(data.session?.spentMicrousd ?? 0)} of ${chatMoney(data.session?.ceilingMicrousd ?? 0)}</span><span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`}</div>`,
|
|
970
|
+
data.session === null
|
|
971
|
+
? html `<p class="meta">No conversation yet. Send a message to start one.</p>`
|
|
972
|
+
: html `<p class="meta">Started ${data.session.mintedAt.slice(0, 16).replace("T", " ")}Z. It stays open until you end it; only bounded recent context is sent to the model.</p>`,
|
|
973
|
+
data.session === null ? "" : postForm("/chat/mate/end", html `<button type="submit" class="quiet">End the conversation and forget the thread</button>`, { attrs: { class: "inline" }, returnTo }),
|
|
967
974
|
data.recent.length === 0
|
|
968
975
|
? ""
|
|
969
976
|
: html `<p class="meta">Recent turns: ${joinHtml(data.recent
|
|
@@ -996,8 +1003,8 @@ export function leadPage(chrome, data) {
|
|
|
996
1003
|
html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${LEAD_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
|
|
997
1004
|
html `<button type="submit" aria-label="${data.pending === null ? "send message" : "send message — it waits for the current reply"}">Send</button>`,
|
|
998
1005
|
], "\n"), {
|
|
999
|
-
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session":
|
|
1000
|
-
hidden: { request: randomBytes(16).toString("hex"), "request-session":
|
|
1006
|
+
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session": sessionId, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": leadChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
|
|
1007
|
+
hidden: { request: randomBytes(16).toString("hex"), "request-session": sessionId, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
|
|
1001
1008
|
}),
|
|
1002
1009
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
1003
1010
|
// a state to report — a reply in progress here, the connection from
|
|
@@ -1009,7 +1016,7 @@ export function leadPage(chrome, data) {
|
|
|
1009
1016
|
return screen("chat", chatWorkspace(joinHtml(conversation, "\n"), data.projects, false, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_CONTINUITY_SCRIPT + CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: true },
|
|
1010
1017
|
workspace: {
|
|
1011
1018
|
conversation: {
|
|
1012
|
-
sessionId
|
|
1019
|
+
sessionId, user: approver, version: leadChatVersion(data),
|
|
1013
1020
|
messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
1014
1021
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
|
|
1015
1022
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
@@ -14,7 +14,7 @@ import { type CriterionMatrixRow } from "../proof.js";
|
|
|
14
14
|
import { type QualityMode } from "../quality.js";
|
|
15
15
|
import { type Runner } from "../runner.js";
|
|
16
16
|
import { type Scope, type UnattendedPermissionMode } from "../scope.js";
|
|
17
|
-
import { type Decision, type Incident, type RepairChainRow, type Run, type Store, type
|
|
17
|
+
import { type Decision, type Incident, type RepairChainRow, type Run, type Store, type TaskState, type WorktreeRow } from "../store.js";
|
|
18
18
|
import { tally } from "../summary.js";
|
|
19
19
|
import { type WorkIndexGroup, type WorkIndexPage } from "../work-index.js";
|
|
20
20
|
import { type WorkFacts, type WorkStatus, type WorkView } from "../workspace-ui.js";
|
|
@@ -284,8 +284,8 @@ export declare function workPage(chrome: Chrome, data: {
|
|
|
284
284
|
multiProject: boolean;
|
|
285
285
|
now: Date;
|
|
286
286
|
limits?: BrowserLimits | null;
|
|
287
|
+
state?: TaskState | null;
|
|
287
288
|
}): Screen;
|
|
288
|
-
export declare function tasksPage(chrome: Chrome, tasks: Task[], state: TaskState | null, problem: string | null, repo?: string | null, prefill?: TaskComposerPrefill | null, permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, replaced?: ReadonlyMap<string, string>): Screen;
|
|
289
289
|
export declare function browsePage(chrome: Chrome, data: {
|
|
290
290
|
at: string;
|
|
291
291
|
root: string;
|
|
@@ -492,13 +492,17 @@ export declare function fleetScript(): string;
|
|
|
492
492
|
* (finding 18); missing that, a select counts clean.
|
|
493
493
|
*/
|
|
494
494
|
export declare function queueScript(): string;
|
|
495
|
-
export declare function newTaskPage(chrome: Chrome, project: string | null,
|
|
495
|
+
export declare function newTaskPage(chrome: Chrome, project: string | null,
|
|
496
|
+
/** undefined: keep the revision the refused form posted (its draft), so a project switch since still refuses. */
|
|
497
|
+
projectRevision: number | undefined, problem: string | null, candidates?: {
|
|
496
498
|
id: string;
|
|
497
499
|
title: string;
|
|
498
500
|
}[], permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, projects?: {
|
|
499
501
|
path: string;
|
|
500
502
|
name: string;
|
|
501
|
-
}[]
|
|
503
|
+
}[],
|
|
504
|
+
/** A shipped template's fields, or a refused filing's own words, back in the form. */
|
|
505
|
+
prefill?: TaskComposerPrefill | null): Screen;
|
|
502
506
|
/**
|
|
503
507
|
* The triage flow: everything waiting on a person, one card at a time.
|
|
504
508
|
* Full context ON the card, the act inline, and every act lands back here
|
|
@@ -287,6 +287,10 @@ export function boardBody(data, ciRed) {
|
|
|
287
287
|
return `${Math.round(minutes / 60)}h`;
|
|
288
288
|
return `${Math.round(minutes / (24 * 60))}d`;
|
|
289
289
|
};
|
|
290
|
+
// A lane's head is a status chip in its own tone (after ReUI's badge with a dot): amber only for the lane that
|
|
291
|
+
// waits on a person, blue for building, green for done, neutral otherwise. The word carries the state.
|
|
292
|
+
const LANE_CHIP = { attention: "chip-needs", building: "chip-live", done: "chip-done", queued: "", waiting: "" };
|
|
293
|
+
const laneChip = (key, title) => html `<span class="chip ${LANE_CHIP[key]}"><span class="chip-dot" aria-hidden="true"></span>${title}</span>`;
|
|
290
294
|
const lane = (key, title, hint, renderOne) => {
|
|
291
295
|
const cards = data.cards.filter(card => card.lane === key);
|
|
292
296
|
// The longest-stalled card leads the board: attention sorts by how
|
|
@@ -312,20 +316,21 @@ export function boardBody(data, ciRed) {
|
|
|
312
316
|
// A lane is a section that can fold (board pass): open when it holds
|
|
313
317
|
// cards, folded when empty, so a phone reads the counts first and a
|
|
314
318
|
// desktop column never spends its height on "nothing here".
|
|
315
|
-
return html `<details class="lane lane-${key}"${shown.length === 0 ? null : html ` open`}><summary><h2>${title} <span class="lane-count">${cards.length}${data.saturated ? "+" : ""}</span></h2></summary><p class="hint">${hint}</p>${shown.length === 0 ? html `<p class="meta lane-empty">nothing here</p>` : shown.map((card, index) => renderOne(card, index))}${more > 0 ? html `<a class="lane-more" href="/
|
|
319
|
+
return html `<details class="lane lane-${key}"${shown.length === 0 ? null : html ` open`}><summary><h2>${laneChip(key === "attention" && cards.length === 0 ? "queued" : key, title)} <span class="lane-count">${cards.length}${data.saturated ? "+" : ""}</span></h2></summary><p class="hint">${hint}</p>${shown.length === 0 ? html `<p class="meta lane-empty">nothing here</p>` : shown.map((card, index) => renderOne(card, index))}${more > 0 ? html `<a class="lane-more" href="/work">+${more} more in Tasks</a>` : null}</details>`;
|
|
316
320
|
};
|
|
317
321
|
// The card's facts: mono key–value pairs under the title (board pass) —
|
|
318
322
|
// task, worker, runtime — the same grammar on every lane, so the eye
|
|
319
323
|
// learns one card. Chips carry the words (project, reservation).
|
|
324
|
+
// A value is mono only when a machine said it (a worker, model or branch name, a clock); words stay Geist.
|
|
320
325
|
const facts = (rows) => rows.length === 0
|
|
321
326
|
? null
|
|
322
|
-
: html `<span class="facts">${rows.map(([k, v]) => html `<span class="fact"><span class="k">${k}</span><span class="v">${v}</span></span>`)}</span>`;
|
|
327
|
+
: html `<span class="facts">${rows.map(([k, v, mono]) => html `<span class="fact"><span class="k">${k}</span><span class="${mono === undefined ? "v" : "v mono"}">${v}</span></span>`)}</span>`;
|
|
323
328
|
const chips = (parts) => {
|
|
324
329
|
const kept = parts.filter((one) => one !== null);
|
|
325
330
|
return kept.length === 0 ? null : html `<span class="chips">${joinHtml(kept, " ")}</span>`;
|
|
326
331
|
};
|
|
327
|
-
const plain = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="
|
|
328
|
-
...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince)]]),
|
|
332
|
+
const plain = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="t">${card.title}</span><span class="why">${card.reason}</span><span class="id">${card.taskId}</span>${facts([
|
|
333
|
+
...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince), "mono"]]),
|
|
329
334
|
])}${chips([
|
|
330
335
|
chip(card.repo),
|
|
331
336
|
])}</a>`;
|
|
@@ -342,8 +347,8 @@ export function boardBody(data, ciRed) {
|
|
|
342
347
|
}
|
|
343
348
|
return heads;
|
|
344
349
|
})();
|
|
345
|
-
const queuedCard = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="
|
|
346
|
-
|
|
350
|
+
const queuedCard = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="t">${card.title}</span><span class="why">${card.reason}</span><span class="id">${card.taskId}</span>${facts([
|
|
351
|
+
card.assignedRunner === null ? ["worker", "any free worker"] : ["worker", card.assignedRunner, "mono"],
|
|
347
352
|
])}${chips([
|
|
348
353
|
queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
|
|
349
354
|
? html `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${card.assignedRunner}`}</span>`
|
|
@@ -367,13 +372,15 @@ export function boardBody(data, ciRed) {
|
|
|
367
372
|
const steps = step === null
|
|
368
373
|
? null
|
|
369
374
|
: html `<span class="steps" role="img" aria-label="${`step ${step + 1} of ${BUILD_STEPS.length}: ${BUILD_STEPS[step]}`}">${BUILD_STEPS.map((_, index) => html `<i class="${index < step ? "on" : index === step ? "now" : ""}"></i>`)}</span>`;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
// Who, which model and which branch, as one muted line: words in Geist, the model and branch in mono.
|
|
376
|
+
const model = claim.model === null ? null : `${claim.provider !== null && claim.provider !== "claude" ? `${claim.provider} · ` : ""}${claim.model}`;
|
|
377
|
+
const branch = claim.branch === null ? null : `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`;
|
|
378
|
+
const who = joinHtml([
|
|
379
|
+
html `<span>${claim.runner}</span>`,
|
|
380
|
+
model === null ? null : html `<span class="mono">${model}</span>`,
|
|
381
|
+
branch === null ? null : html `<span class="mono">${branch}</span>`,
|
|
382
|
+
].filter((one) => one !== null), html `<span aria-hidden="true"> · </span>`);
|
|
383
|
+
return html `<a class="lane-card building" href="${card.href}" data-vt="${card.taskId}"><span class="t"><span class="dot dot-ok pulse"></span>${card.title}</span>${steps}<span class="live-line"><span class="stage">${live}</span>${tickingClock(claim.claimedAt, data.now)}</span><span class="id">${card.taskId}</span><span class="who">${who}</span>${chips([
|
|
377
384
|
card.attempt === null ? null : html `<span class="badge">Attempt ${card.attempt}</span>`,
|
|
378
385
|
chip(card.repo),
|
|
379
386
|
])}</a>`;
|
|
@@ -384,8 +391,8 @@ export function boardBody(data, ciRed) {
|
|
|
384
391
|
const pr = row.prNumber === null
|
|
385
392
|
? null
|
|
386
393
|
: html `<span class="badge badge-open">PR #${row.prNumber}</span>${ciRed(row.prNumber) ? html ` <span class="badge badge-failed">CI failing</span>` : null}`;
|
|
387
|
-
return html `<a class="lane-card" href="${taskHref(row.taskId)}" data-vt="${row.taskId}"><span class="
|
|
388
|
-
...(row.ranMinutes === null ? [] : [["ran", `${row.ranMinutes}m
|
|
394
|
+
return html `<a class="lane-card" href="${taskHref(row.taskId)}" data-vt="${row.taskId}"><span class="t">${row.title}</span>${row.handoff === null ? null : html `<span class="why">${row.handoff.length > 120 ? row.handoff.slice(0, 120) + "…" : row.handoff}</span>`}<span class="id">${row.taskId}</span>${facts([
|
|
395
|
+
...(row.ranMinutes === null ? [] : [["ran", `${row.ranMinutes}m`, "mono"]]),
|
|
389
396
|
["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
|
|
390
397
|
])}${chips([
|
|
391
398
|
html `<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
|
|
@@ -417,11 +424,11 @@ export function boardBody(data, ciRed) {
|
|
|
417
424
|
toggle,
|
|
418
425
|
workerStrip(data.workers, data.cards, data.now),
|
|
419
426
|
html `<div class="board" data-now="${String(data.now.getTime())}">`,
|
|
420
|
-
lane("attention", "
|
|
421
|
-
lane("queued", "
|
|
422
|
-
lane("waiting", "
|
|
423
|
-
lane("building", "
|
|
424
|
-
html `<details class="lane lane-done"${data.done.length === 0 ? null : html ` open`}><summary><h2
|
|
427
|
+
lane("attention", "Needs you", "These wait for a person.", plain),
|
|
428
|
+
lane("queued", "Queued", "Starts when a worker is free.", queuedCard),
|
|
429
|
+
lane("waiting", "Waiting", "Paused until a time, another task, or a requirement is ready.", plain),
|
|
430
|
+
lane("building", "Building", "One agent per card, in its own workspace.", building),
|
|
431
|
+
html `<details class="lane lane-done"${data.done.length === 0 ? null : html ` open`}><summary><h2>${laneChip("done", html `<a href="/done">Done recently</a>`)}</h2></summary><p class="hint">The most recent. The full list is under Done.</p>${doneCards}</details>`,
|
|
425
432
|
html `</div>`,
|
|
426
433
|
], "\n");
|
|
427
434
|
}
|
|
@@ -503,7 +510,7 @@ export function homePage(chrome, data) {
|
|
|
503
510
|
? joinHtml([
|
|
504
511
|
html `<div class="card">`,
|
|
505
512
|
html `<p><strong>Nothing is queued yet — here is the whole loop:</strong></p>`,
|
|
506
|
-
html `<p>1. <a href="/tasks">Add a task</a> — plain words for work you want done${data.repo === null ? null : html ` in <span class="mono">${data.repo}</span>`}.</p>`,
|
|
513
|
+
html `<p>1. <a href="/tasks/new">Add a task</a> — plain words for work you want done${data.repo === null ? null : html ` in <span class="mono">${data.repo}</span>`}.</p>`,
|
|
507
514
|
html `<p>2. Open it and write its scope — the goal, and what it must not become. Approve exactly that.</p>`,
|
|
508
515
|
html `<p>3. Keep Toolroll running on the builder machine. Approved tasks build unattended, each on its own branch.</p>`,
|
|
509
516
|
html `<p class="meta">When an agent is unsure it stops and asks — those questions land here, under “waiting on you”.</p>`,
|
|
@@ -596,12 +603,15 @@ export function taskComposerHtml(data) {
|
|
|
596
603
|
}
|
|
597
604
|
export const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
|
|
598
605
|
export function workPage(chrome, data) {
|
|
599
|
-
|
|
606
|
+
// The stored-state filter (the old task list's) rides every link but a change of state, which starts on page one.
|
|
607
|
+
const href = (view, cursor, state = data.state ?? null) => {
|
|
600
608
|
const query = new URLSearchParams();
|
|
601
609
|
if (data.projectFilter !== undefined)
|
|
602
610
|
query.set('project', data.projectFilter);
|
|
603
611
|
if (view !== 'all')
|
|
604
612
|
query.set('view', view);
|
|
613
|
+
if (state !== null)
|
|
614
|
+
query.set('state', state);
|
|
605
615
|
if (cursor !== undefined)
|
|
606
616
|
query.set('cursor', cursor);
|
|
607
617
|
return `/work${query.size ? `?${query}` : ''}`;
|
|
@@ -614,16 +624,22 @@ export function workPage(chrome, data) {
|
|
|
614
624
|
: browserWorkActionHref(row);
|
|
615
625
|
const action = actionHref === null || row.primaryAction === null ? null : html `<a class="work-action" data-primary-action href="${actionHref}">${row.primaryAction.label} →</a>`;
|
|
616
626
|
const project = data.multiProject || row.repo === null ? html `<span class="project-label">${row.repo === null ? 'Unplaced' : projectName(row.repo)}</span>` : null;
|
|
617
|
-
return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
|
|
627
|
+
return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span><span class="mono">${row.rootId}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
|
|
618
628
|
};
|
|
619
629
|
const current = WORK_VIEWS.find(one => one.key === data.view);
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
630
|
+
const filtered = (data.state ?? null) !== null;
|
|
631
|
+
// The state filter, as real links (the old task list's), so a page without the React view can still change it.
|
|
632
|
+
const stateLinks = html `<nav class="work-states" aria-label="Task state"><span class="meta">State:</span> ${joinHtml([html `<a href="${href(data.view, undefined, null)}"${filtered ? null : html ` aria-current="page"`}>Any</a>`,
|
|
633
|
+
...TASK_STATES.map(one => html `<a href="${href(data.view, undefined, one)}"${data.state === one ? html ` aria-current="page"` : null}>${one}</a>`)], html ` · `)}</nav>`;
|
|
634
|
+
const list = data.work.items.length === 0 && filtered && !data.previous
|
|
635
|
+
? html `<div class="work-empty" data-work-empty="${data.view}"><p>No tasks are ${data.state} in this view.</p><a href="${href(data.view, undefined, null)}">Show every state →</a></div>`
|
|
636
|
+
: data.work.items.length === 0
|
|
637
|
+
? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
|
|
638
|
+
: html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
|
|
623
639
|
const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
|
|
624
|
-
const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/
|
|
640
|
+
const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
|
|
625
641
|
.map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
|
|
626
|
-
const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/
|
|
642
|
+
const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
|
|
627
643
|
const view = {
|
|
628
644
|
kind: 'tasks',
|
|
629
645
|
tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
|
|
@@ -649,7 +665,7 @@ export function workPage(chrome, data) {
|
|
|
649
665
|
notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
|
|
650
666
|
};
|
|
651
667
|
}),
|
|
652
|
-
empty: data.work.items.length > 0 ? null : {
|
|
668
|
+
empty: data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: "Show every state", href: href(data.view, undefined, null) } } : {
|
|
653
669
|
text: data.previous ? 'There are no more tasks on this page.' : current.empty,
|
|
654
670
|
action: data.view === 'all' && !data.previous ? { label: chrome.chat === true ? 'Start in chat' : 'Add a task', href: chrome.chat === true ? '/chat' : '/tasks/new' } : { label: 'See all tasks', href: href('all') },
|
|
655
671
|
},
|
|
@@ -657,26 +673,10 @@ export function workPage(chrome, data) {
|
|
|
657
673
|
tools: toolLinks.map(([path, label]) => ({ label: label, href: path })),
|
|
658
674
|
newTask: { label: 'New task', href: '/tasks/new' },
|
|
659
675
|
limits: data.limits ?? null,
|
|
676
|
+
states: [{ label: 'Any state', href: href(data.view, undefined, null), active: (data.state ?? null) === null },
|
|
677
|
+
...TASK_STATES.map(one => ({ label: one.charAt(0).toUpperCase() + one.slice(1), href: href(data.view, undefined, one), active: data.state === one }))],
|
|
660
678
|
};
|
|
661
|
-
return screen('work', html `<div class="work-head"><h1>Tasks</h1>${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${list}${pages}`, { chrome, workspace: { view } });
|
|
662
|
-
}
|
|
663
|
-
export function tasksPage(chrome, tasks, state, problem, repo = null, prefill = null, permissionDefault = "auto", qualityDefault = "default", replaced = new Map()) {
|
|
664
|
-
const filters = joinHtml(TASK_STATES.map(one => (one === state ? html `<strong>${one}</strong>` : html `<a href="/tasks?state=${one}">${one}</a>`)), " · ");
|
|
665
|
-
const rows = tasks.length === 0
|
|
666
|
-
? html `<p class="meta">${state === null
|
|
667
|
-
? "The queue is empty — add the first task below. It builds once you approve its scope."
|
|
668
|
-
: `Nothing is ${state}.`}</p>`
|
|
669
|
-
: joinHtml(tasks.map(task => html `<a class="row" href="${taskHref(task.id)}"><span class="mono">${task.id}</span> ${task.title} <span class="right badge badge-${task.state}">${task.state === "cancelled" && replaced.has(task.id) ? `replaced by ${replaced.get(task.id)}` : task.state}</span></a>`), "\n");
|
|
670
|
-
return screen("tasks", joinHtml([
|
|
671
|
-
html `<h1>Tasks</h1>`,
|
|
672
|
-
html `<p class="meta">Work you want done${repo === null ? null : html ` in <strong>${projectName(repo)}</strong>`} — a task builds unattended only after its scope is approved; open one to write or approve its scope</p>`,
|
|
673
|
-
repo === null ? null : html `<p class="meta path-words"><span class="mono">${repo}</span></p>`,
|
|
674
|
-
problem === null ? null : html `<div class="problem">${problem}</div>`,
|
|
675
|
-
html `<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
|
|
676
|
-
rows,
|
|
677
|
-
html `<h2>Add a task</h2>`,
|
|
678
|
-
taskComposerHtml({ project: repo, prefill, permissionDefault, qualityDefault }),
|
|
679
|
-
], "\n"), { chrome });
|
|
679
|
+
return screen('work', html `<div class="work-head"><h1>Tasks</h1>${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
|
|
680
680
|
}
|
|
681
681
|
export function browsePage(chrome, data) {
|
|
682
682
|
const crumb = data.at === data.root ? projectName(data.root) : `${projectName(data.root)}${data.at.slice(data.root.length)}`;
|
|
@@ -1273,7 +1273,11 @@ export function queueScript() {
|
|
|
1273
1273
|
`.then(function(){tell();});});` +
|
|
1274
1274
|
`})();`);
|
|
1275
1275
|
}
|
|
1276
|
-
export function newTaskPage(chrome, project,
|
|
1276
|
+
export function newTaskPage(chrome, project,
|
|
1277
|
+
/** undefined: keep the revision the refused form posted (its draft), so a project switch since still refuses. */
|
|
1278
|
+
projectRevision, problem, candidates = [], permissionDefault = "auto", qualityDefault = "default", projects = [],
|
|
1279
|
+
/** A shipped template's fields, or a refused filing's own words, back in the form. */
|
|
1280
|
+
prefill = null) {
|
|
1277
1281
|
return screen("New task", joinHtml([
|
|
1278
1282
|
html `<section class="task-intake">`,
|
|
1279
1283
|
html `<div class="task-intake-hero"><h1>What should get done?</h1></div>`,
|
|
@@ -1281,7 +1285,8 @@ export function newTaskPage(chrome, project, projectRevision, problem, candidate
|
|
|
1281
1285
|
problem === null ? null : html `<div class="problem">${problem}</div>`,
|
|
1282
1286
|
taskComposerHtml({
|
|
1283
1287
|
project,
|
|
1284
|
-
projectRevision,
|
|
1288
|
+
...(projectRevision === undefined ? {} : { projectRevision }),
|
|
1289
|
+
prefill,
|
|
1285
1290
|
candidates,
|
|
1286
1291
|
permissionDefault,
|
|
1287
1292
|
qualityDefault,
|
|
@@ -170,7 +170,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
|
|
|
170
170
|
: joinHtml([
|
|
171
171
|
html `<h2>Unattended permissions</h2>`,
|
|
172
172
|
permissionDefault.canManage && csrf !== ""
|
|
173
|
-
? postForm("/settings/permission-default", html `${permissionModeChoices("permission-mode", permissionDefault.mode)}<button type="submit">Save default</button>`, { attrs: { class: "card permission-policy"
|
|
173
|
+
? postForm("/settings/permission-default", html `${permissionModeChoices("permission-mode", permissionDefault.mode)}${permissionDefault.mode === "bypassPermissions" ? "" : reauth(html `<label>Your password <span class="meta">(needed for Full access)</span><input type="password" name="password" autocomplete="current-password"></label>`)}<button type="submit">Save default</button>`, { attrs: { class: "card permission-policy" } })
|
|
174
174
|
: html `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">An approver can change this default</p></div>`,
|
|
175
175
|
permissionDefault.updatedAt === null
|
|
176
176
|
? ""
|