toolroll 0.9.59 → 0.9.61
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 +26 -0
- package/README.md +8 -6
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +2 -1
- package/dist/assignment-adapters.js +1 -1
- package/dist/assignment-delivery.d.ts +16 -0
- package/dist/assignment-delivery.js +54 -28
- package/dist/assignment-status.d.ts +14 -5
- package/dist/assignment-status.js +70 -39
- package/dist/assignment.d.ts +10 -2
- package/dist/assignment.js +21 -14
- package/dist/bin.js +11 -2
- package/dist/brief-fence.d.ts +9 -0
- package/dist/brief-fence.js +22 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +178 -29
- package/dist/chat-channel.js +10 -1
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli-fast.d.ts +8 -0
- package/dist/cli-fast.js +18 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.js +91 -0
- package/dist/cli.d.ts +6 -5
- package/dist/cli.js +17 -96
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/design/tokens.css +234 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord.js +2 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +28 -0
- package/dist/evidence.js +87 -17
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/flow-actions.js +2 -1
- package/dist/guides.js +14 -11
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +157 -0
- package/dist/landed-cleanup.js +707 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +49 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +48 -8
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/mailbox.js +3 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/mcp-core.js +1 -1
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +201 -19
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-delete-ui.d.ts +11 -0
- package/dist/project-delete-ui.js +29 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +73 -8
- package/dist/provider.js +87 -17
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +22 -3
- package/dist/server/chrome.d.ts +6 -3
- package/dist/server/chrome.js +8 -4
- package/dist/server/console.css +9 -64
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +14 -2
- package/dist/server/render-tasks.d.ts +14 -2
- package/dist/server/render-tasks.js +38 -20
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +2 -1
- package/dist/server/task-views.js +10 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/slack-socket.d.ts +12 -0
- package/dist/slack-socket.js +29 -0
- package/dist/slack.d.ts +0 -7
- package/dist/slack.js +2 -24
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-mailbox.d.ts +39 -0
- package/dist/steer-mailbox.js +207 -0
- package/dist/steer-status.d.ts +25 -0
- package/dist/steer-status.js +44 -0
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +41 -0
- package/dist/storage-sweep-process.js +187 -0
- package/dist/storage-sweep.d.ts +30 -6
- package/dist/storage-sweep.js +214 -28
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +16 -4
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +202 -2
- package/dist/store.js +440 -13
- package/dist/surface.js +5 -3
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/team-updates.js +3 -3
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +15 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +3 -1
- package/dist/worktree.js +93 -10
- package/package.json +21 -21
- package/scripts/provider-canary.mjs +235 -147
package/dist/server/console.css
CHANGED
|
@@ -30,83 +30,28 @@
|
|
|
30
30
|
font-family: "Geist Mono"; font-style: normal; font-weight: 600;
|
|
31
31
|
font-display: swap; src: url("/fonts/geist-mono-600.woff2") format("woff2");
|
|
32
32
|
}
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
/* Signal's colour, shadow, radius, type and spacing tokens (--so-*) are
|
|
34
|
+
* generated by youi into src/design/tokens.css, light and dark, and the
|
|
35
|
+
* server prepends that sheet to this one (chrome.ts STYLE). Edit them in the
|
|
36
|
+
* design-language repo, never here. The device decides the scheme unless the
|
|
37
|
+
* person pins one (html[data-theme], set from their cookie). What follows are
|
|
38
|
+
* the console's older names: views onto those tokens, never a second ramp. */
|
|
38
39
|
:root {
|
|
39
|
-
color-scheme: light;
|
|
40
|
-
|
|
41
|
-
--so-ground: #efefef; --so-paper: #ffffff; --so-sidebar: #efefef; --so-raised: #f5f5f5;
|
|
42
|
-
--so-ink: #171717; --so-muted: #666666; --so-line: #e6e6e6; --so-input-line: #d4d4d4;
|
|
43
|
-
--so-accent: #171717; --so-accent-hover: #383838; --so-accent-text: #171717; --so-on-accent: #ffffff; --so-soft: #f2f2f2;
|
|
44
|
-
--so-nav-ink: #525252; --so-nav-hover: #e4e4e4; --so-nav-current: #ffffff; --so-nav-current-ink: #171717;
|
|
45
|
-
--so-signal: #171717; --so-signal-hover: #383838; --so-on-signal: #ffffff; --so-signal-soft: #e8e8e8;
|
|
46
|
-
--so-danger: #c4320a; --so-danger-soft: #feebe7; --so-success: #218358; --so-success-soft: #e6f6eb;
|
|
47
|
-
--so-warning: #ab6400; --so-warning-soft: #fff4d5; --so-info: #0d74ce; --so-info-soft: #e6f4fe;
|
|
48
|
-
--so-neutral-ink: #525252; --so-neutral-soft: #f0f0f0; --so-live: #0d74ce;
|
|
49
|
-
--so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
|
|
50
|
-
--so-sheet-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .04), 0 4px 12px -6px rgb(0 0 0 / .06);
|
|
51
|
-
--so-pill-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .06);
|
|
52
|
-
--so-overlay: rgb(0 0 0 / .32); --so-shadow-overlay: 0 0 0 1px rgb(0 0 0 / .08), 0 24px 48px -12px rgb(0 0 0 / .22);
|
|
53
|
-
--so-selection: #d4d4d4; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;
|
|
54
|
-
|
|
55
40
|
--background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);
|
|
56
41
|
--muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);
|
|
57
42
|
--primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised); --secondary-foreground: var(--so-ink);
|
|
58
43
|
--accent: var(--so-soft); --destructive: var(--so-danger); --destructive-strong: var(--so-danger); --destructive-soft: var(--so-danger-soft);
|
|
59
44
|
--success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);
|
|
60
45
|
--running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);
|
|
61
|
-
/*
|
|
46
|
+
/* The accent means one thing: a person is needed (approve, answer, decide). */
|
|
62
47
|
--brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);
|
|
63
48
|
--glass: var(--so-paper); --glass-strong: var(--so-paper); --glass-border: var(--so-line); --glass-highlight: transparent;
|
|
64
49
|
--ambient-one: transparent; --ambient-two: transparent; --user-message: var(--so-user-bubble);
|
|
65
50
|
--surface: var(--so-paper); --ok: var(--so-success); --danger: var(--so-danger); --fg-muted: var(--so-muted);
|
|
66
51
|
--shadow: none; --shadow-overlay: var(--so-shadow-overlay);
|
|
67
52
|
--radius: 0.625rem;
|
|
68
|
-
--
|
|
69
|
-
--font-
|
|
70
|
-
--font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
71
|
-
/* Phone spacing steps (760px and narrower): side gutter, gap under the
|
|
72
|
-
header, gap between page blocks, between rows, and inside a card. */
|
|
73
|
-
--so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
|
|
74
|
-
}
|
|
75
|
-
@media (prefers-color-scheme: dark) {
|
|
76
|
-
:root:not([data-theme="light"]) {
|
|
77
|
-
color-scheme: dark;
|
|
78
|
-
|
|
79
|
-
--so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;
|
|
80
|
-
--so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;
|
|
81
|
-
--so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;
|
|
82
|
-
--so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;
|
|
83
|
-
--so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;
|
|
84
|
-
--so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);
|
|
85
|
-
--so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);
|
|
86
|
-
--so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;
|
|
87
|
-
--so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
|
|
88
|
-
--so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);
|
|
89
|
-
--so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);
|
|
90
|
-
--so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);
|
|
91
|
-
--so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
:root[data-theme="dark"] {
|
|
95
|
-
color-scheme: dark;
|
|
96
|
-
|
|
97
|
-
--so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;
|
|
98
|
-
--so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;
|
|
99
|
-
--so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;
|
|
100
|
-
--so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;
|
|
101
|
-
--so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;
|
|
102
|
-
--so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);
|
|
103
|
-
--so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);
|
|
104
|
-
--so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;
|
|
105
|
-
--so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
|
|
106
|
-
--so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);
|
|
107
|
-
--so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);
|
|
108
|
-
--so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);
|
|
109
|
-
--so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;
|
|
53
|
+
--font-sans: var(--so-font-sans);
|
|
54
|
+
--font-mono: var(--so-font-mono);
|
|
110
55
|
}
|
|
111
56
|
* { box-sizing: border-box; }
|
|
112
57
|
/* Touch: no grey flash on a tap, no double-tap zoom wait on a control,
|
package/dist/server/live.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export type LiveRoom = {
|
|
|
20
20
|
} | {
|
|
21
21
|
kind: "team";
|
|
22
22
|
conversation: string | null;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "agent";
|
|
25
|
+
run: number;
|
|
23
26
|
};
|
|
24
27
|
/** A room's name, read strictly: anything else is not a room. */
|
|
25
28
|
export declare function parseLiveRoom(name: string): LiveRoom | null;
|
package/dist/server/live.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { boardRunOf, holdAgentView, releaseAgentView } from "../agent-board.js";
|
|
3
|
+
import { agentRunFingerprint } from "../agents-board.js";
|
|
1
4
|
import { createLiveRooms, LiveConnection } from "../live-bus.js";
|
|
2
5
|
import { reproveRemote } from "../operate-remote.js";
|
|
3
6
|
import { rowVisible } from "../project.js";
|
|
@@ -8,7 +11,7 @@ import { adapterPolicy } from "./route-policy.js";
|
|
|
8
11
|
import { requestContext } from "./request-context.js";
|
|
9
12
|
/** A page asks for a few rooms at once: its workspace, what it shows, its conversation. */
|
|
10
13
|
export const LIVE_ROOMS_PER_PAGE = 5;
|
|
11
|
-
const ROOM_NAME = /^(workspace|task|flow|chat|team)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
14
|
+
const ROOM_NAME = /^(workspace|task|flow|chat|team|agent)(?::([^?]{1,400}))?(?:\?([^#]{0,600}))?$/;
|
|
12
15
|
/** At most one workspace nudge a second: a burst of writes is one re-read, the first heard at once. */
|
|
13
16
|
const WORKSPACE_SETTLE_MS = 1_000;
|
|
14
17
|
/** A running reply's preview: at most one snapshot every 80 ms; the last one always goes out. */
|
|
@@ -45,6 +48,8 @@ export function parseLiveRoom(name) {
|
|
|
45
48
|
if (key !== null || !only("conversation"))
|
|
46
49
|
return null;
|
|
47
50
|
return { kind, conversation: params.get("conversation") || null };
|
|
51
|
+
case "agent":
|
|
52
|
+
return key !== null && /^[1-9][0-9]{0,9}$/.test(key) && query === undefined ? { kind, run: Number(key) } : null;
|
|
48
53
|
}
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
@@ -61,6 +66,8 @@ export function createLiveHandlers(runtime, live) {
|
|
|
61
66
|
return cursor === null ? null : String(cursor);
|
|
62
67
|
}, null, { bus: live.bus });
|
|
63
68
|
const chatViewers = new Set();
|
|
69
|
+
// One agent's open view (Agents board): what it shows, keyed by the exact run.
|
|
70
|
+
const agentRooms = createLiveRooms(run => agentRunFingerprint(store, run), null, { bus: live.bus });
|
|
64
71
|
/** The same session, still signed in, from the cookie and the account (never this request's context). */
|
|
65
72
|
const readBrowser = (request, who) => {
|
|
66
73
|
const again = identify(request, false);
|
|
@@ -177,7 +184,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
177
184
|
if ([...url.searchParams.keys()].some(key => key !== "room") || names.length === 0 || names.length > LIVE_ROOMS_PER_PAGE || new Set(names).size !== names.length)
|
|
178
185
|
return json(response, 400, { error: "room" });
|
|
179
186
|
const rooms = names.map(name => ({ name, room: parseLiveRoom(name) }));
|
|
180
|
-
if (rooms.some(one => one.room === null) || 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)
|
|
187
|
+
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)
|
|
181
188
|
return json(response, 400, { error: "room" });
|
|
182
189
|
// Admit every room before the stream opens, with the answers each page's own stream gave.
|
|
183
190
|
const joins = [];
|
|
@@ -198,6 +205,31 @@ export function createLiveHandlers(runtime, live) {
|
|
|
198
205
|
// A sign-out or a narrowed account stops it.
|
|
199
206
|
valid: () => signedIn() && rowVisible(liveCeiling(), repo) && store.accountCanAccess(who.name, repo) }));
|
|
200
207
|
}
|
|
208
|
+
else if (room.kind === "agent") {
|
|
209
|
+
const browser = who;
|
|
210
|
+
const run = store.getRun(room.run);
|
|
211
|
+
const repo = run === null ? null : store.refForId(run.taskRef)?.repo ?? null;
|
|
212
|
+
if (run === null || repo === null || !visible(repo) || !store.accountCanAccess(who.name, repo))
|
|
213
|
+
return json(response, 404, { error: "run" });
|
|
214
|
+
// A repair is its build's own activity: its room is the build's.
|
|
215
|
+
const signedIn = sameBrowser(request, browser), runId = boardRunOf(store, run.id), view = randomBytes(12).toString("hex");
|
|
216
|
+
// The watch is a short lease in the database, renewed on every keep-alive and released when the page goes:
|
|
217
|
+
// the process that sends chat pings reads it there (agent-board.ts watchingAgent).
|
|
218
|
+
const watch = () => { try {
|
|
219
|
+
holdAgentView(store, view, who.name, runId, clock());
|
|
220
|
+
}
|
|
221
|
+
catch { /* a missed renewal only lets a ping through */ } };
|
|
222
|
+
joins.push(connection => {
|
|
223
|
+
watch();
|
|
224
|
+
connection.onKeepAlive(watch);
|
|
225
|
+
connection.onClose(() => { try {
|
|
226
|
+
releaseAgentView(store, view, clock());
|
|
227
|
+
}
|
|
228
|
+
catch { /* it lapses on its own */ } });
|
|
229
|
+
agentRooms.join(runId, { name: who.name, room: name, connection,
|
|
230
|
+
valid: () => signedIn() && rowVisible(liveCeiling(), repo) && store.accountCanAccess(who.name, repo) });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
201
233
|
else if (room.kind === "flow") {
|
|
202
234
|
const browser = who;
|
|
203
235
|
const flow = store.getFlow(room.flow);
|
|
@@ -302,6 +334,7 @@ export function createLiveHandlers(runtime, live) {
|
|
|
302
334
|
detachChat();
|
|
303
335
|
workspaceRooms.close();
|
|
304
336
|
teamRooms.close();
|
|
337
|
+
agentRooms.close();
|
|
305
338
|
for (const viewer of [...chatViewers])
|
|
306
339
|
viewer.connection.close();
|
|
307
340
|
},
|
package/dist/server/pages.js
CHANGED
|
@@ -21,7 +21,8 @@ import { run as execRun } from "../exec.js";
|
|
|
21
21
|
import { computeGaps } from "../gaps.js";
|
|
22
22
|
import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
23
23
|
import { ledgerBody } from "../ledger-view.js";
|
|
24
|
-
import {
|
|
24
|
+
import { agentRowOf, agentsBoardOf, agentFactsFor, sealedDiffOf } from "../agents-board.js";
|
|
25
|
+
import { markAgentSeen } from "../agent-board.js";
|
|
25
26
|
import { prometheusMetrics } from "../metrics.js";
|
|
26
27
|
import { readMonitoring } from "../monitoring-settings.js";
|
|
27
28
|
import { targetOf } from "../monitoring.js";
|
|
@@ -374,42 +375,64 @@ export function createPagesHandlers(runtime) {
|
|
|
374
375
|
now,
|
|
375
376
|
}));
|
|
376
377
|
}
|
|
377
|
-
//
|
|
378
|
-
// ceiling admits, each fed by the run page's own transcript poller
|
|
379
|
-
// (the byte-offset JSON protocol — text into textContent, never
|
|
380
|
-
// markup). The pane SET is re-checked on a slow cadence; when it
|
|
381
|
-
// changes the page reloads rather than swapping pollers mid-flight.
|
|
378
|
+
// Peek became the Agents board: one place to check on every running agent (agents-board.ts).
|
|
382
379
|
async function peekPage(ctx) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
380
|
+
return redirect(ctx.response, "/agents");
|
|
381
|
+
}
|
|
382
|
+
/** What the board says after a Reply or a steer, in fixed words (never text from the address). */
|
|
383
|
+
const AGENT_SAID = {
|
|
384
|
+
replied: { tone: "info", text: "Reply sent. The agent reads it at its next check." },
|
|
385
|
+
steered: { tone: "info", text: "Note sent. The agent reads it at its next check." },
|
|
386
|
+
"reply-closed": { tone: "error", text: "That question was already answered." },
|
|
387
|
+
"reply-stale": { tone: "error", text: "This attempt ended before your reply, so nothing was sent." },
|
|
388
|
+
"reply-invalid": { tone: "error", text: "Write a reply of up to 1,500 characters." },
|
|
389
|
+
};
|
|
390
|
+
// The Agents board (one place to check on running agents): rows by state, and the opened agent's live view.
|
|
391
|
+
async function agentsPage(ctx) {
|
|
392
|
+
const { url, who, response, project } = ctx;
|
|
388
393
|
const now = clock();
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
394
|
+
const access = workAccess();
|
|
395
|
+
const asked = url.searchParams.get("run");
|
|
396
|
+
let selected = null;
|
|
397
|
+
if (asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
398
|
+
const run = store.getRun(Number(asked));
|
|
399
|
+
const facts = run === null || !runVisible(run) ? null : agentFactsFor(store, run.id, access, who.name);
|
|
400
|
+
if (facts !== null) {
|
|
401
|
+
// Opening a finished agent is viewing it: it leaves this person's unread Done.
|
|
402
|
+
if (facts.outcome !== null && markAgentSeen(store, who.name, facts.runId, now))
|
|
403
|
+
facts.seen = true;
|
|
404
|
+
const live = facts.outcome === null;
|
|
405
|
+
const sealed = live ? null : sealedDiffOf(store, evidenceRoot, facts.runId);
|
|
406
|
+
// A reply's own word first; otherwise the note that the agent asked past the questions cap.
|
|
407
|
+
const said = AGENT_SAID[url.searchParams.get("said") ?? ""] ?? (facts.questionNote === null ? null : { tone: "info", text: facts.questionNote });
|
|
408
|
+
selected = {
|
|
409
|
+
...agentRowOf(store, facts, now, evidenceRoot), live,
|
|
410
|
+
question: live && facts.question !== null ? { ...facts.question, action: `${taskHref(facts.taskId)}/reply` } : null,
|
|
411
|
+
transcript: options.localRunner === undefined ? null : `/r/${facts.runId}?fragment=transcript`,
|
|
412
|
+
// The live look at its checkout needs this machine's runner and pool, as the run page's does.
|
|
413
|
+
peek: live && options.localRunner !== undefined && options.poolRoot !== undefined ? `/r/${facts.runId}?fragment=peek` : null,
|
|
414
|
+
files: (sealed?.files ?? []).slice(0, 30).map(one => ({ path: one.path, added: one.additions, removed: one.deletions })),
|
|
415
|
+
filesMore: Math.max(0, (sealed?.fileCount ?? 0) - 30),
|
|
416
|
+
steer: live ? { action: `${taskHref(facts.taskId)}/steer`, run: facts.runId } : null,
|
|
417
|
+
canAct: who.role === "approver" && !store.isDemo(),
|
|
418
|
+
taskHref: taskHref(facts.taskId),
|
|
419
|
+
// The run's own address: a finished result opens on its result page, anything else on its record.
|
|
420
|
+
resultHref: live ? null : `/r/${facts.runId}`,
|
|
421
|
+
room: `agent:${facts.runId}`, said,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const groups = agentsBoardOf(store, now, access, who.name, evidenceRoot);
|
|
426
|
+
const view = { kind: "agents", groups, selected,
|
|
427
|
+
empty: groups.length === 0 ? "No agents in the last day. They show here the moment one starts." : null };
|
|
428
|
+
const fallback = joinHtml([
|
|
429
|
+
html `<h1>Agents</h1>`,
|
|
430
|
+
groups.length === 0 ? html `<p class="meta">${view.empty ?? ""}</p>` : joinHtml(groups.map(group => joinHtml([
|
|
431
|
+
html `<h2>${group.label}</h2>`,
|
|
432
|
+
...group.rows.map(row => html `<p class="row"><a href="${row.href}"><strong>${row.title}</strong></a> <span class="meta">${[row.label, row.line].filter(Boolean).join(" \u00b7 ")}</span></p>`),
|
|
433
|
+
], "\n")), "\n"),
|
|
434
|
+
], "\n");
|
|
435
|
+
return sendScreen(response, 200, screen("Agents", fallback, { chrome: chromeFor(project, "runs"), workspace: { view } }));
|
|
413
436
|
}
|
|
414
437
|
async function fleetPage(ctx) {
|
|
415
438
|
const { url, who, response, project } = ctx;
|
|
@@ -901,6 +924,7 @@ export function createPagesHandlers(runtime) {
|
|
|
901
924
|
"activity": activityPage,
|
|
902
925
|
"system": systemScreen,
|
|
903
926
|
"peek": peekPage,
|
|
927
|
+
"agents": agentsPage,
|
|
904
928
|
"fleet": fleetPage,
|
|
905
929
|
"ledger.export": ledgerExport,
|
|
906
930
|
"menu": menuPage,
|
|
@@ -494,7 +494,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
494
494
|
reserve: { label: "Worker assignment", action: payload["worker"] === null ? "release" : "reserve", icon: html `<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>` },
|
|
495
495
|
hold: { label: "Pause work", action: "hold", icon: html `<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>` },
|
|
496
496
|
unhold: { label: "Resume work", action: "release hold", icon: html `<path d="m7 4 13 8-13 8z"/>` },
|
|
497
|
-
steer: { label: "Guidance for
|
|
497
|
+
steer: { label: "Guidance for the agent", action: "add guidance", icon: html `<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>` },
|
|
498
498
|
scope: { label: "Scope revision", action: "save scope", icon: html `<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4z"/>` },
|
|
499
499
|
answer: { label: "Decision answer", action: "confirm answer", icon: html `<path d="M9.1 9a3 3 0 1 1 5.8 1c0 2-3 2-3 4"/><path d="M12 18h.01"/><circle cx="12" cy="12" r="9"/>` },
|
|
500
500
|
cancel: { label: "Cancel task", action: "open task", icon: html `<path d="m15 9-6 6"/><path d="m9 9 6 6"/><circle cx="12" cy="12" r="9"/>` },
|
|
@@ -570,9 +570,21 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
570
570
|
else if (view.kind === "unhold") {
|
|
571
571
|
what = html `<h3>Release <a href="${taskHref(task)}">${task}</a> from its hold</h3>${facts(["project", mono(repoId)])}`;
|
|
572
572
|
}
|
|
573
|
+
else if (view.kind === "steer" && payload["mode"] === "pause") {
|
|
574
|
+
const taskTitle = text("taskTitle") || task;
|
|
575
|
+
what = html `<h3>Pause <a href="${taskHref(task)}">${taskTitle}</a></h3>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">Its current turn stops and the build keeps running; its next note continues it.</p>`;
|
|
576
|
+
}
|
|
577
|
+
else if (view.kind === "steer" && payload["mode"] === "resume") {
|
|
578
|
+
const taskTitle = text("taskTitle") || task;
|
|
579
|
+
what = html `<h3>Resume <a href="${taskHref(task)}">${taskTitle}</a></h3>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">It goes on where it stopped. No note is sent.</p>`;
|
|
580
|
+
}
|
|
581
|
+
else if (view.kind === "steer" && payload["mode"] === "pause-and-steer") {
|
|
582
|
+
const taskTitle = text("taskTitle") || task;
|
|
583
|
+
what = html `<h3>Pause <a href="${taskHref(task)}">${taskTitle}</a> and send a note</h3><p class="proposal-summary">${text("note")}</p>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">Its current turn stops first. The task’s scope doesn’t change.</p>`;
|
|
584
|
+
}
|
|
573
585
|
else if (view.kind === "steer") {
|
|
574
586
|
const taskTitle = text("taskTitle") || task;
|
|
575
|
-
what = html `<h3>
|
|
587
|
+
what = html `<h3>Guidance for <a href="${taskHref(task)}">${taskTitle}</a></h3><p class="proposal-summary">${text("note")}</p>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">A running agent reads it when it next checks for notes; otherwise the next attempt does. The task’s scope doesn’t change.</p>`;
|
|
576
588
|
}
|
|
577
589
|
else if (view.kind === "agents") {
|
|
578
590
|
// The agents card (v48): exactly what changes, in the same words the
|
|
@@ -264,10 +264,18 @@ export declare function taskStatusCard(status: DisplayStatus & {
|
|
|
264
264
|
diagnostics?: WorkStatus["diagnostics"];
|
|
265
265
|
}, taskId: string, dispatch: DispatchDiagnosis | null, runId: number | null, approvalAction?: boolean): Html;
|
|
266
266
|
export declare function checkProgressHtml(progress: CheckProgress | null, id?: string): Html;
|
|
267
|
-
export declare function taskControlDetailsHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean): Html;
|
|
267
|
+
export declare function taskControlDetailsHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean, livePause?: LivePauseFact | null): Html;
|
|
268
268
|
/** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details.
|
|
269
269
|
* `csrf` is only a guard now: an empty one (a read-only surface) renders no forms; postForm writes the field. */
|
|
270
|
-
|
|
270
|
+
/** Live steering: the Claude build that takes Pause right now, its latest pause (null: none, or it continued), and
|
|
271
|
+
* until when a paused build can wait before it ends (null: no idle limit). */
|
|
272
|
+
export type LivePauseFact = {
|
|
273
|
+
run: number;
|
|
274
|
+
state: "pausing" | "paused" | null;
|
|
275
|
+
until?: string | null;
|
|
276
|
+
limitMs?: number | null;
|
|
277
|
+
};
|
|
278
|
+
export declare function taskControlHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean, livePause?: LivePauseFact | null): Html;
|
|
271
279
|
/** The resume confirmation (v52): the exact run restated, what resuming
|
|
272
280
|
* does and does not do, the gate that would still keep work from
|
|
273
281
|
* starting, and the password typed again. */
|
|
@@ -489,6 +497,8 @@ export declare function taskBodyParts(data: {
|
|
|
489
497
|
liveRunId?: number | null;
|
|
490
498
|
/** What the agent did last on that live run, and when (task-activity.ts). */
|
|
491
499
|
activity?: RunActivity | null;
|
|
500
|
+
/** Live steering: the Claude build that takes Pause right now, and its latest pause (null: none, or it continued). */
|
|
501
|
+
livePause?: LivePauseFact | null;
|
|
492
502
|
/** Workers that are both alive and authorized for this task's project. */
|
|
493
503
|
worker?: {
|
|
494
504
|
answering: number;
|
|
@@ -570,6 +580,8 @@ export declare function taskBodyParts(data: {
|
|
|
570
580
|
} | null;
|
|
571
581
|
/** Operator steering notes (arc 1), delivery state included. */
|
|
572
582
|
steering?: SteerNote[];
|
|
583
|
+
/** A build's agent is running now and reads new notes when it next checks for them (v120). */
|
|
584
|
+
steeringLive?: boolean;
|
|
573
585
|
/** The publication grant the publisher would act under — from
|
|
574
586
|
* publicationGrantFor(repo) only; null when none, or no project. */
|
|
575
587
|
grant?: PublicationGrant | null;
|
|
@@ -34,8 +34,10 @@ import { runCostWords } from "../summary.js";
|
|
|
34
34
|
import {} from "../task-control.js";
|
|
35
35
|
import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_MORE, taskStatusOf } from "../task-status.js";
|
|
36
36
|
import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, workStatusOf } from "../workspace-ui.js";
|
|
37
|
-
import { createHash } from "node:crypto";
|
|
37
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
38
|
+
import { TEXT_LIMITS } from "../text-limits.js";
|
|
38
39
|
import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, transcriptScript, whenTime } from "./chrome.js";
|
|
40
|
+
import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
|
|
39
41
|
import { reviewHref, taskChatHref, taskHref } from "./http.js";
|
|
40
42
|
import { coordinatorProposalsSection } from "./render-chat.js";
|
|
41
43
|
import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, terminalDiffCard, verificationExplanation, verificationRecovered } from "./render-results.js";
|
|
@@ -807,14 +809,26 @@ export function checkProgressHtml(progress, id = "check-progress") {
|
|
|
807
809
|
: Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
|
|
808
810
|
return html `<p id="${id}" class="check-progress mono" data-check-progress data-final="${state}" role="status" aria-live="polite">${progress.line}</p>`;
|
|
809
811
|
}
|
|
810
|
-
export function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
811
|
-
const card = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
|
|
812
|
+
export function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false, livePause = null) {
|
|
813
|
+
const card = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere, livePause);
|
|
812
814
|
return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
|
|
813
815
|
? html `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${card}</details>` : card;
|
|
814
816
|
}
|
|
815
|
-
/**
|
|
816
|
-
*
|
|
817
|
-
|
|
817
|
+
/** Pause and Pause and steer beside the task page's Stop (live steering): the same exact run, each form under its own
|
|
818
|
+
* request id so a repeated submit is the same request. Pause hides once asked; a paused build says until when it can
|
|
819
|
+
* wait and offers Resume, or the note to continue with. */
|
|
820
|
+
function pauseFormsHtml(taskId, livePause) {
|
|
821
|
+
const form = (fields, button, control, extra = {}) => postForm(`${taskHref(taskId)}/pause`, html `${fields}${button}`, { attrs: { class: "inline task-control-form", "data-task-control": control }, returnTo: "task", hidden: { run: livePause.run, request: `web-${randomUUID()}`, ...extra } });
|
|
822
|
+
const paused = livePause.state === "paused";
|
|
823
|
+
const until = livePause.until ?? null, limitMs = livePause.limitMs ?? null;
|
|
824
|
+
return joinHtml([
|
|
825
|
+
paused ? html `<p class="meta" role="status" data-live-pause="paused">${until === null || limitMs === null ? "Paused." : html `Paused. It can wait until ${whenTime(until)} (${Math.max(1, Math.round(limitMs / 60_000))} min) before it ends.`}</p>` : "",
|
|
826
|
+
livePause.state === null ? form(html ``, html `<button type="submit" class="task-control-button">Pause</button>`, "pause") : "",
|
|
827
|
+
paused ? form(html ``, html `<button type="submit" class="task-control-button">Resume</button>`, "resume", { mode: "resume" }) : "",
|
|
828
|
+
form(html `<input type="text" name="note" required maxlength="${TEXT_LIMITS.note}" placeholder="${paused ? "note to continue with" : "note for the agent"}" aria-label="${paused ? "note to continue with" : "note for the agent"}">`, html `<button type="submit" class="task-control-button">${paused ? "Continue" : "Pause and steer"}</button>`, "pause-and-steer"),
|
|
829
|
+
]);
|
|
830
|
+
}
|
|
831
|
+
export function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false, livePause = null) {
|
|
818
832
|
if (control.kind === "none")
|
|
819
833
|
return html ``;
|
|
820
834
|
const guarded = csrf !== "" && !inert;
|
|
@@ -832,6 +846,7 @@ export function taskControlHtml(control, taskId, csrf, surface, inert = false, s
|
|
|
832
846
|
return joinHtml([
|
|
833
847
|
html `<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">`,
|
|
834
848
|
html `<div class="task-control-copy"><details><summary>Stop details · ${role(control)} #${control.run}</summary><p class="meta">Stopping ends only this attempt's own processes. Its branch, uncommitted work, evidence, and decisions stay preserved; other tasks keep running.</p></details></div>`,
|
|
849
|
+
stopElsewhere || !guarded || livePause === null || livePause.run !== control.run ? "" : pauseFormsHtml(taskId, livePause),
|
|
835
850
|
stopElsewhere ? "" : guarded
|
|
836
851
|
? runForm("stop", "inline task-control-form task-stop-form", html `<button type="submit" class="danger task-control-button">Stop</button>`)
|
|
837
852
|
: html `<button type="button" class="danger task-control-button" disabled>Stop</button>`,
|
|
@@ -1370,24 +1385,21 @@ export function taskBodyParts(data) {
|
|
|
1370
1385
|
}
|
|
1371
1386
|
return box("problem", "Dispatch unknown", "Refresh this task before relying on its scheduler state.", "unknown");
|
|
1372
1387
|
})();
|
|
1373
|
-
// Operator steering (arc 1): notes for the agent, each wearing
|
|
1374
|
-
//
|
|
1388
|
+
// Operator steering (arc 1, v120): notes for the agent, each wearing the
|
|
1389
|
+
// same words the CLI and the lead use — read (and when), waiting for the
|
|
1390
|
+
// running agent, bound for the next attempt, or ended unread.
|
|
1375
1391
|
const steering = data.steering ?? [];
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
? `reached build #${one.attachedRun}`
|
|
1382
|
-
: one.attachedRun !== null
|
|
1383
|
-
? `attached to build #${one.attachedRun} — delivery not yet proven`
|
|
1384
|
-
: "waiting for the next attempt";
|
|
1392
|
+
const steeringLive = data.steeringLive === true;
|
|
1393
|
+
const steerState = (one) => {
|
|
1394
|
+
const { words, readAt } = steerStandingParts(one, steeringLive);
|
|
1395
|
+
return readAt === null ? html `${words}` : html `${words} ${whenTime(readAt)}`;
|
|
1396
|
+
};
|
|
1385
1397
|
const steerRows = steering.length === 0 ? "" : joinHtml(steering.map(one => html `<p class="row"><span class="meta">${one.author} · ${whenTime(one.createdAt)} · ${steerState(one)}</span> ${one.note}</p>`), "\n");
|
|
1386
1398
|
const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
|
|
1387
1399
|
? ""
|
|
1388
1400
|
: joinHtml([
|
|
1389
|
-
postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the
|
|
1390
|
-
html `<p class="meta"
|
|
1401
|
+
postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the agent" aria-label="steering note" style="width:100%;max-width:28rem"><button type="submit">Steer</button>`, { attrs: { class: "row" } }),
|
|
1402
|
+
html `<p class="meta">${steerDestinationWords(steeringLive)} A note can't widen the approved scope.</p>`,
|
|
1391
1403
|
]);
|
|
1392
1404
|
const steeringCard = steerRows === "" && steerForm === "" ? "" : html `<h2>Steering</h2>${steerRows}${steerForm}`;
|
|
1393
1405
|
const holds = data.holds.length === 0
|
|
@@ -1896,7 +1908,7 @@ export function taskBodyParts(data) {
|
|
|
1896
1908
|
const statusHtml = html `${data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && data.dispatch?.action === "approve-scope")}${checkProgressHtml(data.checkProgress ?? null)}`;
|
|
1897
1909
|
// The exact-run control (v52), directly under the scheduler's answer:
|
|
1898
1910
|
// the one place a person stops or resumes THIS attempt.
|
|
1899
|
-
const controlHtml = taskControlDetailsHtml(data.control ?? { kind: "none" }, task.id, data.csrf, "task");
|
|
1911
|
+
const controlHtml = taskControlDetailsHtml(data.control ?? { kind: "none" }, task.id, data.csrf, "task", false, false, data.livePause ?? null);
|
|
1900
1912
|
const previousResult = data.completion?.receipt == null || receiptLeads ? null : {
|
|
1901
1913
|
title: data.assignment != null && task.state === "done" ? "Result" : "Previous result",
|
|
1902
1914
|
html: (completionReceiptCard(data.completion.receipt, task.id, "task", data.assignment == null ? receiptStatusOf(data.completion.receipt) : assignmentStatusOf(data.assignment), data.assignment ?? null, data.assignment == null)),
|
|
@@ -2177,6 +2189,11 @@ export function taskBodyParts(data) {
|
|
|
2177
2189
|
// The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
|
|
2178
2190
|
const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
|
|
2179
2191
|
? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
|
|
2192
|
+
// Pause and Pause and steer sit beside that Stop, for the same run, when it is a Claude build that takes live input.
|
|
2193
|
+
// Each form carries its own request id, so a repeated submit is the same request and the two never collide.
|
|
2194
|
+
const pause = stop !== null && data.livePause?.run === stop.run
|
|
2195
|
+
? { action: `${taskHref(task.id)}/pause`, run: stop.run, state: data.livePause.state, requests: [`web-${randomUUID()}`, `web-${randomUUID()}`, `web-${randomUUID()}`],
|
|
2196
|
+
until: data.livePause.until ?? null, limitMs: data.livePause.limitMs ?? null } : null;
|
|
2180
2197
|
// The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
|
|
2181
2198
|
const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
|
|
2182
2199
|
const earlier = earlierStopped.length === 0 ? null : {
|
|
@@ -2206,6 +2223,7 @@ export function taskBodyParts(data) {
|
|
|
2206
2223
|
// One quiet line under the step: what the agent did last, and when.
|
|
2207
2224
|
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
2208
2225
|
stop,
|
|
2226
|
+
pause,
|
|
2209
2227
|
record,
|
|
2210
2228
|
earlier,
|
|
2211
2229
|
approval: fragmentString(approvalHtml),
|
|
@@ -129,7 +129,8 @@ get("people", "people.page", "/people", { limited: "unscoped", ...free });
|
|
|
129
129
|
get("pages", "system", "/system");
|
|
130
130
|
get("tasks", "tasks", "/tasks", { limited: "collection" });
|
|
131
131
|
get("tasks", "queue", "/queue");
|
|
132
|
-
get("pages", "peek", "/peek");
|
|
132
|
+
get("pages", "peek", "/peek", free);
|
|
133
|
+
get("pages", "agents", "/agents", { limited: "unscoped", ...free });
|
|
133
134
|
get("pages", "fleet", "/fleet", free);
|
|
134
135
|
get("tasks", "tasks.new", "/tasks/new", { limited: "collection", ...free });
|
|
135
136
|
get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
|
|
@@ -330,6 +331,7 @@ post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "
|
|
|
330
331
|
post("tasks", "task.act.next", `^\\/t\\/${TASK}\\/next$`, { project: "task", limited: "resource", sample: "/t/one/next" });
|
|
331
332
|
post("tasks", "task.act.reopen", `^\\/t\\/${TASK}\\/reopen$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reopen" });
|
|
332
333
|
post("tasks", "task.act.steer", `^\\/t\\/${TASK}\\/steer$`, { project: "task", limited: "resource", sample: "/t/one/steer" });
|
|
334
|
+
post("tasks", "task.act.reply", `^\\/t\\/${TASK}\\/reply$`, { project: "task", limited: "resource", sample: "/t/one/reply" });
|
|
333
335
|
post("tasks", "task.act.accept-proof", `^\\/t\\/${TASK}\\/accept-proof$`, { project: "task", limited: "resource", sample: "/t/one/accept-proof" });
|
|
334
336
|
post("tasks", "task.act.accept-revision", `^\\/t\\/${TASK}\\/accept-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/accept-revision" });
|
|
335
337
|
post("tasks", "task.act.reject-revision", `^\\/t\\/${TASK}\\/reject-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reject-revision" });
|
|
@@ -339,6 +341,7 @@ post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "ta
|
|
|
339
341
|
post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
|
|
340
342
|
post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
|
|
341
343
|
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
344
|
+
post("tasks", "task.act.pause", `^\\/t\\/${TASK}\\/pause$`, { project: "task", limited: "resource", sample: "/t/one/pause" });
|
|
342
345
|
post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
|
|
343
346
|
post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
|
|
344
347
|
for (const one of ["block", "unblock", "repair-dependency", "follow-up"])
|
package/dist/server/settings.js
CHANGED
|
@@ -105,6 +105,7 @@ import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from
|
|
|
105
105
|
import { ssoSettingsHtml } from "../sso-ui.js";
|
|
106
106
|
import { lastSweep, saveSweep, storageSweepOff } from "../storage-sweep.js";
|
|
107
107
|
import { storageHtml } from "../storage-ui.js";
|
|
108
|
+
import { landedSummary } from "../landed-cleanup.js";
|
|
108
109
|
import { bytesWords, parseCleanup } from "../storage.js";
|
|
109
110
|
import { isDigestTime, RESULT_SCREENSHOTS } from "../store.js";
|
|
110
111
|
import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
|
|
@@ -398,7 +399,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
398
399
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
399
400
|
const plan = await checkoutPlan(store, storagePool(databaseFile), now, { manual: true });
|
|
400
401
|
const sweep = { last: lastSweep(store), off: storageSweepOff() };
|
|
401
|
-
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
402
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
402
403
|
}
|
|
403
404
|
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
404
405
|
// exactly what turning on would do, or what to fix first.
|
|
@@ -418,6 +418,15 @@ export function createTaskViews(runtime) {
|
|
|
418
418
|
const found = runtime.store.runsFor(ref.id).find(one => runIsLive(one));
|
|
419
419
|
return found === undefined ? null : found.id;
|
|
420
420
|
})(),
|
|
421
|
+
// Live steering: the Claude build that takes Pause right now, and where its latest pause stands.
|
|
422
|
+
livePause: (() => {
|
|
423
|
+
const run = ref === null ? null : runtime.store.liveInputRun(ref.id, now);
|
|
424
|
+
if (run === null)
|
|
425
|
+
return null;
|
|
426
|
+
const pause = runtime.store.livePauseOf(run);
|
|
427
|
+
const until = pause?.pausedAt == null || pause.limitMs === null ? null : new Date(Date.parse(pause.pausedAt) + pause.limitMs).toISOString();
|
|
428
|
+
return { run, state: pause?.state ?? null, until, limitMs: pause?.limitMs ?? null };
|
|
429
|
+
})(),
|
|
421
430
|
worker: (() => {
|
|
422
431
|
const runners = runtime.store.listRunners().filter(one => one.retiredAt === null);
|
|
423
432
|
const eligible = ref?.repo === null || ref?.repo === undefined
|
|
@@ -473,6 +482,7 @@ export function createTaskViews(runtime) {
|
|
|
473
482
|
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
474
483
|
})(),
|
|
475
484
|
steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
|
|
485
|
+
steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
|
|
476
486
|
// "publishes as" reads ONLY publicationGrantFor(repo) — the grant
|
|
477
487
|
// the publisher would act under — never listGrants(), which is
|
|
478
488
|
// dispatch authority (slice 1c).
|