@a-t-h-i/bot-lobby 0.6.49 → 0.6.50
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/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/package.json +9 -7
- package/src/classifier/classifier.ts +14 -2
- package/src/classifier/hosts.ts +12 -18
- package/src/classifier/instance.ts +7 -4
- package/src/execution/delegation.ts +2 -1
- package/src/execution/pi-runner.ts +11 -2
- package/src/lobby/host.ts +6 -0
- package/src/lobby/keys.ts +2 -1
- package/src/lobby/service.ts +71 -4
- package/src/lobby/sessions.ts +35 -0
- package/src/pi/auto-compaction.ts +46 -19
- package/src/pi/events.ts +2 -0
- package/src/pi/fresh-context.ts +10 -8
- package/src/pi/plan-checklist.ts +47 -15
- package/src/schemas/task.ts +14 -0
- package/src/state/persistence.ts +4 -1
- package/src/webui/api/index.ts +36 -2
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/plan-facts.ts +9 -4
- package/src/webui/api/sessions.ts +21 -0
- package/src/webui/api/tasks.ts +12 -1
- package/src/webui/api/terminal.ts +42 -0
- package/src/webui/dev/fake-service.ts +42 -1
- package/src/webui/protocol.ts +29 -2
- package/src/webui/server.ts +16 -1
- package/src/webui/terminal.ts +292 -0
- package/src/workflow/workflow.ts +3 -2
- package/webui/dist/assets/TerminalView-BrP-ENHg.css +1 -0
- package/webui/dist/assets/TerminalView-D_j85u4O.js +16 -0
- package/webui/dist/assets/dm-sans-latin-wght-normal-Xz1IZZA0.woff2 +0 -0
- package/webui/dist/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
- package/webui/dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/webui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/webui/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/webui/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/webui/dist/assets/index-BkM--P3x.js +115 -0
- package/webui/dist/assets/index-DrHXotS7.css +2 -0
- package/webui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/webui/dist/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
- package/webui/dist/assets/nunito-latin-wght-normal-BzFMHfZw.woff2 +0 -0
- package/webui/dist/assets/outfit-latin-wght-normal-Bc-8i84L.woff2 +0 -0
- package/webui/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/webui/dist/assets/roboto-mono-latin-wght-normal-CZtBPCCa.woff2 +0 -0
- package/webui/dist/assets/source-code-pro-latin-wght-normal-BP8Zz55n.woff2 +0 -0
- package/webui/dist/assets/source-sans-3-latin-wght-normal-Bo_j9asI.woff2 +0 -0
- package/webui/dist/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/webui/dist/build.json +108 -92
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-C3p3zxoV.js +0 -114
- package/webui/dist/assets/index-DE9jHz-C.css +0 -2
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminals in the web lobby: each one a real shell in the project's folder,
|
|
3
|
+
* on a pseudo-terminal so prompts, colours and full-screen programs work.
|
|
4
|
+
* The shell keeps running while no page watches it (a reload reattaches and
|
|
5
|
+
* replays what it printed), and ends when it exits, when the page closes it,
|
|
6
|
+
* or when the server stops.
|
|
7
|
+
*
|
|
8
|
+
* The pseudo-terminal comes from `node-pty` when it is installed (it is not a
|
|
9
|
+
* dependency: it needs a native build), and otherwise from a few lines of
|
|
10
|
+
* Python's standard `pty` module, which macOS and Linux ship. With neither
|
|
11
|
+
* (Windows without node-pty, or no python3) the terminal says why it is off.
|
|
12
|
+
*
|
|
13
|
+
* The page writes with `terminal.input` / `terminal.resize` and reads
|
|
14
|
+
* `GET /api/terminal.stream?id=…`: a `reset` with the scrollback first, then
|
|
15
|
+
* `data` as the shell prints and `exit` when it ends.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
18
|
+
import { basename } from "node:path";
|
|
19
|
+
import { StringDecoder } from "node:string_decoder";
|
|
20
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
21
|
+
import type { TerminalInfo, TerminalStreamEvent } from "./protocol.ts";
|
|
22
|
+
|
|
23
|
+
/** Terminals open at once; one more is refused. */
|
|
24
|
+
export const MAX_TERMINALS = 8;
|
|
25
|
+
/** What a terminal keeps of its output for a page that attaches later, in characters. */
|
|
26
|
+
export const SCROLLBACK_CHARS = 256 * 1024;
|
|
27
|
+
/** Largest single write from the page. */
|
|
28
|
+
export const MAX_INPUT_CHARS = 64 * 1024;
|
|
29
|
+
const HEARTBEAT_MS = 15_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The Python side: fork a shell on a new pseudo-terminal, copy stdin to it and
|
|
33
|
+
* it to stdout, and read `cols rows` lines on fd 3 to resize it (the kernel
|
|
34
|
+
* then tells the shell). When stdin closes the terminal is hung up; the exit
|
|
35
|
+
* code is the shell's.
|
|
36
|
+
*/
|
|
37
|
+
const PY_PTY = `
|
|
38
|
+
import os, sys, pty, select, struct, fcntl, termios, signal
|
|
39
|
+
cols, rows = int(sys.argv[1]), int(sys.argv[2])
|
|
40
|
+
argv = sys.argv[3:]
|
|
41
|
+
def size(fd, c, r):
|
|
42
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", r, c, 0, 0))
|
|
43
|
+
pid, fd = pty.fork()
|
|
44
|
+
if pid == 0:
|
|
45
|
+
size(0, cols, rows)
|
|
46
|
+
os.execvp(argv[0], argv)
|
|
47
|
+
def write_all(target, data):
|
|
48
|
+
while data:
|
|
49
|
+
try:
|
|
50
|
+
data = data[os.write(target, data):]
|
|
51
|
+
except InterruptedError:
|
|
52
|
+
pass
|
|
53
|
+
inputs, control = [0, fd, 3], b""
|
|
54
|
+
while True:
|
|
55
|
+
try:
|
|
56
|
+
ready = select.select(inputs, [], [])[0]
|
|
57
|
+
except InterruptedError:
|
|
58
|
+
continue
|
|
59
|
+
if fd in ready:
|
|
60
|
+
try:
|
|
61
|
+
data = os.read(fd, 65536)
|
|
62
|
+
except OSError:
|
|
63
|
+
data = b""
|
|
64
|
+
if not data:
|
|
65
|
+
break
|
|
66
|
+
write_all(1, data)
|
|
67
|
+
if 0 in ready:
|
|
68
|
+
data = os.read(0, 65536)
|
|
69
|
+
if not data:
|
|
70
|
+
os.close(fd)
|
|
71
|
+
try:
|
|
72
|
+
os.killpg(pid, signal.SIGHUP)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
break
|
|
76
|
+
write_all(fd, data)
|
|
77
|
+
if 3 in ready:
|
|
78
|
+
data = os.read(3, 1024)
|
|
79
|
+
if not data:
|
|
80
|
+
inputs.remove(3)
|
|
81
|
+
control += data
|
|
82
|
+
while b"\\n" in control:
|
|
83
|
+
line, control = control.split(b"\\n", 1)
|
|
84
|
+
try:
|
|
85
|
+
c, r = (int(part) for part in line.split())
|
|
86
|
+
size(fd, c, r)
|
|
87
|
+
except (ValueError, OSError):
|
|
88
|
+
pass
|
|
89
|
+
status = os.waitpid(pid, 0)[1]
|
|
90
|
+
sys.exit(os.waitstatus_to_exitcode(status) if hasattr(os, "waitstatus_to_exitcode") else status >> 8)
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
/** A pseudo-terminal with a shell on it, whichever way it was made. */
|
|
94
|
+
interface Pty {
|
|
95
|
+
write(data: string): void;
|
|
96
|
+
resize(cols: number, rows: number): void;
|
|
97
|
+
kill(): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface PtyHandlers {
|
|
101
|
+
onData: (text: string) => void;
|
|
102
|
+
onExit: (code: number | undefined) => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface NodePtyModule {
|
|
106
|
+
spawn(file: string, args: string[], options: { name: string; cols: number; rows: number; cwd: string; env: NodeJS.ProcessEnv }): {
|
|
107
|
+
onData(listener: (data: string) => void): void;
|
|
108
|
+
onExit(listener: (event: { exitCode: number }) => void): void;
|
|
109
|
+
write(data: string): void;
|
|
110
|
+
resize(cols: number, rows: number): void;
|
|
111
|
+
kill(): void;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type Backend = { kind: "node-pty"; module: NodePtyModule } | { kind: "python" } | { kind: "none"; reason: string };
|
|
116
|
+
|
|
117
|
+
let backend: Promise<Backend> | undefined;
|
|
118
|
+
|
|
119
|
+
/** How this machine gets a pseudo-terminal, found once. */
|
|
120
|
+
export function terminalBackend(): Promise<Backend> {
|
|
121
|
+
backend ??= (async (): Promise<Backend> => {
|
|
122
|
+
// Named through a variable so the type checker and bundlers leave the optional module alone.
|
|
123
|
+
const optional = "node-pty";
|
|
124
|
+
try {
|
|
125
|
+
const loaded = (await import(optional)) as NodePtyModule & { default?: NodePtyModule };
|
|
126
|
+
const module = typeof loaded.spawn === "function" ? loaded : loaded.default;
|
|
127
|
+
if (module && typeof module.spawn === "function") return { kind: "node-pty", module };
|
|
128
|
+
} catch {
|
|
129
|
+
// Not installed: fall back to Python's pty.
|
|
130
|
+
}
|
|
131
|
+
if (process.platform === "win32") return { kind: "none", reason: "The terminal needs node-pty on Windows: run `npm install -g node-pty` next to bot-lobby." };
|
|
132
|
+
const probe = spawnSync("python3", ["-c", "import pty, termios, fcntl"], { stdio: "ignore", timeout: 5_000 });
|
|
133
|
+
if (probe.status === 0) return { kind: "python" };
|
|
134
|
+
return { kind: "none", reason: "The terminal needs python3 (or node-pty) on this machine." };
|
|
135
|
+
})();
|
|
136
|
+
return backend;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The user's shell and its arguments: a login shell where the shell knows the flag. */
|
|
140
|
+
export function shellCommand(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string[] {
|
|
141
|
+
if (platform === "win32") return [env.COMSPEC || "powershell.exe"];
|
|
142
|
+
const shell = env.SHELL?.trim() || "/bin/bash";
|
|
143
|
+
return ["bash", "zsh", "fish", "ksh"].includes(basename(shell)) ? [shell, "-l"] : [shell];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function terminalEnv(): NodeJS.ProcessEnv {
|
|
147
|
+
return { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor", TERM_PROGRAM: "bot-lobby" };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function spawnPython(cwd: string, cols: number, rows: number, handlers: PtyHandlers): Pty {
|
|
151
|
+
const child: ChildProcess = spawn("python3", ["-c", PY_PTY, String(cols), String(rows), ...shellCommand()], {
|
|
152
|
+
cwd,
|
|
153
|
+
env: terminalEnv(),
|
|
154
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
155
|
+
});
|
|
156
|
+
const decoder = new StringDecoder("utf8");
|
|
157
|
+
child.stdout!.on("data", (chunk: Buffer) => handlers.onData(decoder.write(chunk)));
|
|
158
|
+
child.stderr!.on("data", (chunk: Buffer) => handlers.onData(decoder.write(chunk)));
|
|
159
|
+
const control = child.stdio[3] as NodeJS.WritableStream;
|
|
160
|
+
for (const stream of [child.stdin!, control]) stream.on("error", () => {});
|
|
161
|
+
child.on("error", () => handlers.onExit(undefined));
|
|
162
|
+
child.on("exit", (code) => handlers.onExit(code ?? undefined));
|
|
163
|
+
return {
|
|
164
|
+
write: (data) => { if (child.stdin!.writable) child.stdin!.write(data); },
|
|
165
|
+
resize: (c, r) => { if (control.writable) control.write(`${c} ${r}\n`); },
|
|
166
|
+
kill: () => { child.stdin!.end(); setTimeout(() => { if (child.exitCode === null) child.kill("SIGKILL"); }, 2_000).unref(); },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function spawnNodePty(module: NodePtyModule, cwd: string, cols: number, rows: number, handlers: PtyHandlers): Pty {
|
|
171
|
+
const [file, ...args] = shellCommand();
|
|
172
|
+
const pty = module.spawn(file!, args, { name: "xterm-256color", cols, rows, cwd, env: terminalEnv() });
|
|
173
|
+
pty.onData(handlers.onData);
|
|
174
|
+
pty.onExit((event) => handlers.onExit(event.exitCode));
|
|
175
|
+
return { write: (data) => pty.write(data), resize: (c, r) => pty.resize(c, r), kill: () => pty.kill() };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface Terminal {
|
|
179
|
+
info: TerminalInfo;
|
|
180
|
+
pty: Pty;
|
|
181
|
+
scrollback: string;
|
|
182
|
+
watchers: Set<ServerResponse>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function frame(event: TerminalStreamEvent): string {
|
|
186
|
+
return `data: ${JSON.stringify(event)}\n\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const clampSize = (value: number, max: number) => Math.max(2, Math.min(max, Math.floor(value)));
|
|
190
|
+
|
|
191
|
+
/** Every terminal this server runs. */
|
|
192
|
+
export class TerminalHost {
|
|
193
|
+
private terminals = new Map<string, Terminal>();
|
|
194
|
+
private count = 0;
|
|
195
|
+
|
|
196
|
+
list(): TerminalInfo[] {
|
|
197
|
+
return [...this.terminals.values()].map((terminal) => ({ ...terminal.info }));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Open a terminal in `cwd`; refused when the machine has no way to make one, or too many are open. */
|
|
201
|
+
async open(cwd: string, cols: number, rows: number): Promise<TerminalInfo> {
|
|
202
|
+
const found = await terminalBackend();
|
|
203
|
+
if (found.kind === "none") throw new TerminalError(found.reason);
|
|
204
|
+
if (this.terminals.size >= MAX_TERMINALS) throw new TerminalError(`at most ${MAX_TERMINALS} terminals; close one first`);
|
|
205
|
+
const id = `t${++this.count}`;
|
|
206
|
+
const shell = basename(shellCommand()[0]!);
|
|
207
|
+
const info: TerminalInfo = { id, title: `${shell} ${this.count}`, cwd, alive: true, startedAt: Date.now() };
|
|
208
|
+
const handlers: PtyHandlers = {
|
|
209
|
+
onData: (text) => {
|
|
210
|
+
if (!text) return;
|
|
211
|
+
const terminal = this.terminals.get(id);
|
|
212
|
+
if (!terminal) return;
|
|
213
|
+
terminal.scrollback = (terminal.scrollback + text).slice(-SCROLLBACK_CHARS);
|
|
214
|
+
this.send(terminal, { type: "data", data: text });
|
|
215
|
+
},
|
|
216
|
+
onExit: (code) => {
|
|
217
|
+
const terminal = this.terminals.get(id);
|
|
218
|
+
if (!terminal || !terminal.info.alive) return;
|
|
219
|
+
terminal.info.alive = false;
|
|
220
|
+
if (code !== undefined) terminal.info.exitCode = code;
|
|
221
|
+
this.send(terminal, { type: "exit", ...(code !== undefined ? { code } : {}) });
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const c = clampSize(cols, 500);
|
|
225
|
+
const r = clampSize(rows, 300);
|
|
226
|
+
const pty = found.kind === "node-pty" ? spawnNodePty(found.module, cwd, c, r, handlers) : spawnPython(cwd, c, r, handlers);
|
|
227
|
+
this.terminals.set(id, { info, pty, scrollback: "", watchers: new Set() });
|
|
228
|
+
return { ...info };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
input(id: string, data: string): void {
|
|
232
|
+
const terminal = this.need(id);
|
|
233
|
+
if (terminal.info.alive) terminal.pty.write(data);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
resize(id: string, cols: number, rows: number): void {
|
|
237
|
+
const terminal = this.need(id);
|
|
238
|
+
if (terminal.info.alive) terminal.pty.resize(clampSize(cols, 500), clampSize(rows, 300));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** End a terminal's shell (hung up, as when a terminal window closes) and forget it. */
|
|
242
|
+
close(id: string): void {
|
|
243
|
+
const terminal = this.need(id);
|
|
244
|
+
terminal.pty.kill();
|
|
245
|
+
for (const res of terminal.watchers) res.end();
|
|
246
|
+
this.terminals.delete(id);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** `GET /api/terminal.stream?id=…`: the scrollback, then what the shell prints. */
|
|
250
|
+
stream(req: IncomingMessage, res: ServerResponse, id: string): boolean {
|
|
251
|
+
const terminal = this.terminals.get(id);
|
|
252
|
+
if (!terminal) return false;
|
|
253
|
+
res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-store", Connection: "keep-alive", "X-Accel-Buffering": "no" });
|
|
254
|
+
res.write(`retry: 2000\n${frame({ type: "reset", data: terminal.scrollback, alive: terminal.info.alive, ...(terminal.info.exitCode !== undefined ? { code: terminal.info.exitCode } : {}) })}`);
|
|
255
|
+
terminal.watchers.add(res);
|
|
256
|
+
const beat = setInterval(() => res.write(":ping\n\n"), HEARTBEAT_MS);
|
|
257
|
+
beat.unref();
|
|
258
|
+
req.on("close", () => {
|
|
259
|
+
clearInterval(beat);
|
|
260
|
+
terminal.watchers.delete(res);
|
|
261
|
+
});
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** End every shell (the server is stopping). */
|
|
266
|
+
closeAll(): void {
|
|
267
|
+
for (const id of [...this.terminals.keys()]) this.close(id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private send(terminal: Terminal, event: TerminalStreamEvent): void {
|
|
271
|
+
const text = frame(event);
|
|
272
|
+
for (const res of terminal.watchers) res.write(text);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private need(id: string): Terminal {
|
|
276
|
+
const terminal = this.terminals.get(id);
|
|
277
|
+
if (!terminal) throw new TerminalError(`no terminal ${id}`, true);
|
|
278
|
+
return terminal;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A terminal request that cannot be done: `missing` when the terminal is gone. */
|
|
283
|
+
export class TerminalError extends Error {
|
|
284
|
+
readonly missing: boolean;
|
|
285
|
+
constructor(message: string, missing = false) {
|
|
286
|
+
super(message);
|
|
287
|
+
this.missing = missing;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The server's terminals. */
|
|
292
|
+
export const terminals = new TerminalHost();
|
package/src/workflow/workflow.ts
CHANGED
|
@@ -56,7 +56,7 @@ import { assertNoPendingApprovals, pendingApprovals, requestApproval, resolveApp
|
|
|
56
56
|
import { withBlockingRequest } from "../state/blocking-requests.ts";
|
|
57
57
|
import { pingApproval } from "../pi/notify.ts";
|
|
58
58
|
import { describeRun, runLogEntry } from "../pi/run-summary.ts";
|
|
59
|
-
import { planChecklist, planSteps, runSteps } from "../pi/plan-checklist.ts";
|
|
59
|
+
import { planChecklist, planSteps, runSteps, skippedStepsNote } from "../pi/plan-checklist.ts";
|
|
60
60
|
import { STEP_LIMIT_MS, grantStepTime, reserveStepTime, stepTimeLeft } from "../state/step-limits.ts";
|
|
61
61
|
import { noteWorkerRun } from "../state/work-time.ts";
|
|
62
62
|
import { nextStates } from "./transitions.ts";
|
|
@@ -256,6 +256,7 @@ export function describeTask(task: Task): string {
|
|
|
256
256
|
? `Pending approvals: ${pendingApprovals(task).map((a) => `${a.id} ${a.kind} for ${a.domain}: ${truncate(a.detail, 120)}`).join("; ")}`
|
|
257
257
|
: "",
|
|
258
258
|
task.blockers.length > 0 ? `Blockers: ${task.blockers.map((b) => b.reason).join("; ")}` : "",
|
|
259
|
+
skippedStepsNote(task.plan, task.skippedSteps),
|
|
259
260
|
`Next legal states: ${nextStates(task.state).join(", ")}`,
|
|
260
261
|
];
|
|
261
262
|
return lines.filter((line) => line.length > 0).join("\n");
|
|
@@ -1858,7 +1859,7 @@ async function workerTimes(task: Task, deps: WorkflowDeps, assignments: readonly
|
|
|
1858
1859
|
const now = budgetFor(task, deps);
|
|
1859
1860
|
const slots = openSlots(task, assignments.map((entry) => entry.domain));
|
|
1860
1861
|
const steps = planSteps(task.plan ?? "");
|
|
1861
|
-
const completed = new Set(planChecklist(task.plan ?? "", (task.workerRuns ?? []).map((record) => ({ ...record, taskId: task.id, role: "worker" as const, output: "", attempts: 1 }))).flatMap((step, index) => step.status === "done" ? [index] : []));
|
|
1862
|
+
const completed = new Set(planChecklist(task.plan ?? "", (task.workerRuns ?? []).map((record) => ({ ...record, taskId: task.id, role: "worker" as const, output: "", attempts: 1 })), task.skippedSteps).flatMap((step, index) => step.status === "done" || step.status === "skipped" ? [index] : []));
|
|
1862
1863
|
const prepared = [];
|
|
1863
1864
|
for (const { domain, instruction, minutes } of assignments) {
|
|
1864
1865
|
const covered = runSteps(steps, instruction, completed);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}
|