@a-t-h-i/bot-lobby 0.6.3 → 0.6.5
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 +180 -17
- package/package.json +12 -6
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/master.md +98 -19
- package/prompts/quickfix.md +16 -3
- package/prompts/researcher.md +8 -2
- package/prompts/reviewer.md +42 -0
- package/prompts/worker.md +7 -0
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- package/src/ask/dialog.ts +167 -0
- package/src/ask/image.ts +202 -0
- package/src/ask/png.ts +179 -0
- package/src/ask/relay.ts +89 -0
- package/src/ask/state.ts +160 -0
- package/src/ask/tool.ts +126 -0
- package/src/ask/types.ts +55 -0
- package/src/ask/view.ts +159 -0
- package/src/classifier/triage.ts +24 -7
- package/src/execution/agent-runner.ts +103 -11
- package/src/execution/git.ts +111 -14
- package/src/execution/pi-runner.ts +164 -11
- package/src/index.ts +9 -0
- package/src/lobby/ask.ts +10 -120
- package/src/lobby/feed.ts +6 -1
- package/src/lobby/layout.ts +27 -8
- package/src/lobby/markdown.ts +36 -6
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +50 -3
- package/src/lobby/runtime.ts +41 -40
- package/src/lobby/tabs/home.ts +112 -48
- package/src/lobby/tabs/issues.ts +4 -3
- package/src/lobby/tabs/plan.ts +4 -4
- package/src/lobby/tabs/quickfix.ts +8 -2
- package/src/lobby/tabs/tasks.ts +16 -5
- package/src/lobby/theme.ts +30 -0
- package/src/lobby/view.ts +36 -5
- package/src/master/decisions.ts +10 -2
- package/src/master/master.ts +41 -4
- package/src/master/research.ts +5 -2
- package/src/pi/commands.ts +108 -13
- package/src/pi/events.ts +48 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/route.ts +180 -0
- package/src/pi/start-task.ts +56 -4
- package/src/pi/tools.ts +32 -9
- package/src/pi/ui.ts +6 -1
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +16 -9
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +28 -2
- package/src/schemas/configuration.ts +15 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +52 -0
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/text.ts +28 -2
- package/src/web/extract.ts +332 -0
- package/src/web/fetch.ts +232 -0
- package/src/web/html.ts +183 -0
- package/src/web/read.ts +113 -0
- package/src/web/search.ts +202 -0
- package/src/web/tools.ts +279 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +734 -46
package/src/lobby/tabs/home.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Task } from "../../schemas/task.ts";
|
|
|
10
10
|
import type { ActivityEntry, ChatEntry, ThoughtEntry } from "../feed.ts";
|
|
11
11
|
import type { LobbyPanel } from "../../schemas/configuration.ts";
|
|
12
12
|
import { markdownBlocks } from "../markdown.ts";
|
|
13
|
-
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, since, spinner, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
|
+
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, since, spinner, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout, type PaneMark } from "../layout.ts";
|
|
14
14
|
|
|
15
15
|
/** The Lobby tab's scrollable panes. */
|
|
16
16
|
export const HOME_PANES = ["conversation", "activity", "thinking"] as const;
|
|
@@ -36,6 +36,8 @@ export interface HomeInput {
|
|
|
36
36
|
focus?: HomePane;
|
|
37
37
|
/** Filled with where each pane landed and how much it holds. */
|
|
38
38
|
panes?: PaneLayout;
|
|
39
|
+
/** Each pane's newest entry as the frame before marked it, so the pane can measure what arrived below since. */
|
|
40
|
+
anchors?: ReadonlyMap<string, PaneMark>;
|
|
39
41
|
tick: number;
|
|
40
42
|
now: number;
|
|
41
43
|
/** Which panes show. */
|
|
@@ -144,18 +146,20 @@ export function speakerLine(speaker: Speaker, width: number, theme?: LobbyTheme,
|
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* What you wrote: a bubble on the right, only as wide as its longest line (up
|
|
147
|
-
* to `YOU_SHARE` of the pane), your words in
|
|
148
|
-
* user-message background — or, where the theme
|
|
149
|
-
* by a bar in that colour.
|
|
149
|
+
* to `YOU_SHARE` of the pane), your words in Markdown as pi renders them, in
|
|
150
|
+
* the accent colour on pi's user-message background — or, where the theme
|
|
151
|
+
* has no background, closed by a bar in that colour.
|
|
150
152
|
*/
|
|
151
153
|
export function youLines(text: string, width: number, theme?: LobbyTheme): string[] {
|
|
152
154
|
const most = Math.max(1, messageWidth(width, YOU_SHARE, 4) - 2);
|
|
153
|
-
|
|
155
|
+
// The renderer colours your words itself; without one (tests) they are wrapped and painted here.
|
|
156
|
+
const rendered = theme?.markdown ? markdownLines(text, most, theme, true, "you") : wrap(text, most).map((line) => paint(theme, "accent", line));
|
|
157
|
+
const lines = rendered.length > 0 ? rendered : [""];
|
|
154
158
|
const inner = Math.max(1, ...lines.map((line) => textWidth(line)));
|
|
155
159
|
const pad = " ".repeat(Math.max(0, width - inner - 2));
|
|
156
160
|
const bubble = theme?.bg
|
|
157
|
-
? (line: string) => theme.bg!("userMessageBg", ` ${fit(
|
|
158
|
-
: (line: string) => `${fit(
|
|
161
|
+
? (line: string) => theme.bg!("userMessageBg", ` ${fit(line, inner)} `)
|
|
162
|
+
: (line: string) => `${fit(line, inner)} ${paint(theme, "accent", "▐")}`;
|
|
159
163
|
return lines.map((line) => `${pad}${bubble(line)}`);
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -181,25 +185,19 @@ function openBlockLines(text: string, width: number, theme: LobbyTheme | undefin
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
/**
|
|
184
|
-
* The
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
+
* The reply the oracle is still writing. It is drawn block by block (see
|
|
189
|
+
* `markdownBlocks`): finished blocks come from the Markdown cache and only the
|
|
190
|
+
* one being written is rendered on each frame. It is drawn whole (it is
|
|
191
|
+
* capped at `MAX_REPLY_TEXT`), so the pane knows exactly how far it reaches.
|
|
188
192
|
*/
|
|
189
|
-
function liveLines(text: string, width: number, theme: LobbyTheme | undefined
|
|
193
|
+
function liveLines(text: string, width: number, theme: LobbyTheme | undefined): string[] {
|
|
190
194
|
const blocks = markdownBlocks(text);
|
|
191
|
-
const parts: string[][] = [];
|
|
192
|
-
let count = 0;
|
|
193
|
-
for (let index = blocks.length - 1; index >= 0 && count < need; index -= 1) {
|
|
194
|
-
const lines = index === blocks.length - 1 ? openBlockLines(blocks[index]!, width, theme) : oracleLines(blocks[index]!, width, theme);
|
|
195
|
-
if (lines.length === 0) continue;
|
|
196
|
-
parts.push(lines);
|
|
197
|
-
count += lines.length + 1;
|
|
198
|
-
}
|
|
199
195
|
const lines: string[] = [];
|
|
200
|
-
for (let
|
|
196
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
197
|
+
const block = index === blocks.length - 1 ? openBlockLines(blocks[index]!, width, theme) : oracleLines(blocks[index]!, width, theme);
|
|
198
|
+
if (block.length === 0) continue;
|
|
201
199
|
if (lines.length > 0) lines.push("");
|
|
202
|
-
for (const line of
|
|
200
|
+
for (const line of block) lines.push(line);
|
|
203
201
|
}
|
|
204
202
|
return lines;
|
|
205
203
|
}
|
|
@@ -218,6 +216,20 @@ export function eventLines(text: string, at: number, width: number, theme?: Lobb
|
|
|
218
216
|
export interface PaneLines {
|
|
219
217
|
lines: string[];
|
|
220
218
|
total: number;
|
|
219
|
+
/** The newest entry and how many lines run from its first line to the end, for the next frame to measure against. */
|
|
220
|
+
mark?: PaneMark;
|
|
221
|
+
/** Lines that arrived below the anchor passed in since it was marked; absent when it is no longer there. */
|
|
222
|
+
grew?: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Where the anchor (an entry the frame before marked) sits in `entries`, so
|
|
227
|
+
* drawing goes on at least that far back; `entries.length` when it is gone.
|
|
228
|
+
* It is nearly always the last entry, so the search from the end is short.
|
|
229
|
+
*/
|
|
230
|
+
function reachOf<T extends object>(entries: readonly T[], anchor: PaneMark | undefined): { found: number; reach: number } {
|
|
231
|
+
const found = anchor ? entries.lastIndexOf(anchor.entry as T) : -1;
|
|
232
|
+
return { found, reach: found < 0 ? entries.length : found };
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
/** A message's drawn lines, kept until the pane's width, the theme or its grouping changes. */
|
|
@@ -259,6 +271,18 @@ function entryBlock(entry: ChatEntry, before: ChatEntry | undefined, width: numb
|
|
|
259
271
|
return lines;
|
|
260
272
|
}
|
|
261
273
|
|
|
274
|
+
export interface ChatTailOptions {
|
|
275
|
+
/** The reply the oracle is still writing. */
|
|
276
|
+
live?: string;
|
|
277
|
+
/** The oracle's turn is running. */
|
|
278
|
+
busy?: boolean;
|
|
279
|
+
tick?: number;
|
|
280
|
+
/** Tops a conversation that has earlier messages still to load. */
|
|
281
|
+
older?: string;
|
|
282
|
+
/** The newest message as the frame before marked it: drawing reaches at least back to it, to measure what arrived below. */
|
|
283
|
+
anchor?: PaneMark;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
/**
|
|
263
287
|
* The newest `need` lines of the conversation, like a chat: your messages as
|
|
264
288
|
* bubbles on the right, the oracle's replies in Markdown on the left, each
|
|
@@ -269,7 +293,8 @@ function entryBlock(entry: ChatEntry, before: ChatEntry | undefined, width: numb
|
|
|
269
293
|
* whole conversation has been drawn. `older` tops a conversation that has
|
|
270
294
|
* earlier messages still to load.
|
|
271
295
|
*/
|
|
272
|
-
export function chatTail(chat: readonly ChatEntry[], width: number, theme: LobbyTheme | undefined, need: number,
|
|
296
|
+
export function chatTail(chat: readonly ChatEntry[], width: number, theme: LobbyTheme | undefined, need: number, options: ChatTailOptions = {}): PaneLines {
|
|
297
|
+
const { live, busy = false, tick = 0, older, anchor } = options;
|
|
273
298
|
const parts: string[][] = [];
|
|
274
299
|
let count = 0;
|
|
275
300
|
const add = (lines: string[]) => {
|
|
@@ -277,10 +302,18 @@ export function chatTail(chat: readonly ChatEntry[], width: number, theme: Lobby
|
|
|
277
302
|
count += lines.length + (parts.length > 0 ? 1 : 0);
|
|
278
303
|
parts.push(lines);
|
|
279
304
|
};
|
|
280
|
-
if (live?.trim()) add([speakerLine("oracle", width, theme, `${spinner(tick)} writing`), ...liveLines(live.trim(), width, theme
|
|
305
|
+
if (live?.trim()) add([speakerLine("oracle", width, theme, `${spinner(tick)} writing`), ...liveLines(live.trim(), width, theme)]);
|
|
281
306
|
else if (busy) add([speakerLine("oracle", width, theme, `${spinner(tick)} working…`)]);
|
|
307
|
+
const { found, reach } = reachOf(chat, anchor);
|
|
308
|
+
let mark: PaneMark | undefined;
|
|
309
|
+
let grew: number | undefined;
|
|
282
310
|
let index = chat.length - 1;
|
|
283
|
-
for (; index >= 0 && count < need
|
|
311
|
+
for (; index >= 0 && (count < need || index >= reach); index -= 1) {
|
|
312
|
+
const entry = chat[index]!;
|
|
313
|
+
add(entryBlock(entry, chat[index - 1], width, theme));
|
|
314
|
+
if (index === chat.length - 1) mark = { entry, below: count };
|
|
315
|
+
if (index === found) grew = count - anchor!.below;
|
|
316
|
+
}
|
|
284
317
|
const drawn = chat.length - 1 - index;
|
|
285
318
|
if (index < 0 && older) add(wrap(paint(theme, "dim", older), width));
|
|
286
319
|
const lines: string[] = [];
|
|
@@ -291,12 +324,12 @@ export function chatTail(chat: readonly ChatEntry[], width: number, theme: Lobby
|
|
|
291
324
|
// Messages not drawn yet count at the average height of those that were.
|
|
292
325
|
const rest = index + 1;
|
|
293
326
|
const total = rest > 0 && drawn > 0 ? lines.length + Math.ceil((rest * lines.length) / drawn) : lines.length;
|
|
294
|
-
return { lines, total };
|
|
327
|
+
return { lines, total, ...(mark ? { mark } : {}), ...(grew !== undefined ? { grew } : {}) };
|
|
295
328
|
}
|
|
296
329
|
|
|
297
330
|
/** The whole conversation drawn (the session browser's preview and tests; the Lobby tab draws only what shows). */
|
|
298
331
|
export function chatLines(chat: readonly ChatEntry[], width: number, theme?: LobbyTheme, live?: string, busy = false, tick = 0): string[] {
|
|
299
|
-
return chatTail(chat, width, theme, Number.POSITIVE_INFINITY, live, busy, tick).lines;
|
|
332
|
+
return chatTail(chat, width, theme, Number.POSITIVE_INFINITY, { ...(live ? { live } : {}), busy, tick }).lines;
|
|
300
333
|
}
|
|
301
334
|
|
|
302
335
|
function emptyChat(input: HomeInput, width: number, theme?: LobbyTheme): string[] {
|
|
@@ -334,19 +367,27 @@ function whole(lines: string[]): PaneLines {
|
|
|
334
367
|
return { lines, total: lines.length };
|
|
335
368
|
}
|
|
336
369
|
|
|
337
|
-
function conversation(input: HomeInput, chat: readonly ChatEntry[], width: number, need: number, theme?: LobbyTheme): PaneLines {
|
|
370
|
+
function conversation(input: HomeInput, chat: readonly ChatEntry[], width: number, need: number, anchor: PaneMark | undefined, theme?: LobbyTheme): PaneLines {
|
|
338
371
|
const searching = Boolean(input.query);
|
|
339
372
|
const live = searching ? undefined : input.liveReply;
|
|
340
373
|
const busy = !searching && input.busy;
|
|
341
374
|
if (chat.length === 0 && !live && !busy) return whole(emptyChat(input, width, theme));
|
|
342
|
-
|
|
375
|
+
const older = searching ? undefined : input.olderNote;
|
|
376
|
+
return chatTail(chat, width, theme, need, { ...(live ? { live } : {}), busy, tick: input.tick, ...(older ? { older } : {}), ...(anchor ? { anchor } : {}) });
|
|
343
377
|
}
|
|
344
378
|
|
|
345
379
|
/** The newest `need` activity lines (one per entry), drawn only as far back as the pane shows. */
|
|
346
|
-
function activity(input: HomeInput, entries: readonly ActivityEntry[], width: number, need: number, theme?: LobbyTheme): PaneLines {
|
|
380
|
+
function activity(input: HomeInput, entries: readonly ActivityEntry[], width: number, need: number, anchor: PaneMark | undefined, theme?: LobbyTheme): PaneLines {
|
|
347
381
|
if (entries.length === 0) return whole(wrap(paint(theme, "dim", input.query ? `No activity matches "${input.query}".` : input.activityNote ?? "No activity yet."), width));
|
|
348
382
|
const shown = Number.isFinite(need) ? entries.slice(-Math.max(0, need)) : entries;
|
|
349
|
-
|
|
383
|
+
// One line per entry: where the anchor sits is a count, nothing needs drawing to find it.
|
|
384
|
+
const { found } = reachOf(entries, anchor);
|
|
385
|
+
return {
|
|
386
|
+
lines: shown.map((entry) => activityLine(entry, width, input.tick, theme)),
|
|
387
|
+
total: entries.length,
|
|
388
|
+
mark: { entry: entries.at(-1)!, below: 1 },
|
|
389
|
+
...(found >= 0 ? { grew: entries.length - found - anchor!.below } : {}),
|
|
390
|
+
};
|
|
350
391
|
}
|
|
351
392
|
|
|
352
393
|
/** The last `rows` lines, `offset` lines back from the newest (clamped), and where they start. */
|
|
@@ -381,8 +422,9 @@ export function currentThought(thoughts: readonly ThoughtEntry[]): ThoughtEntry
|
|
|
381
422
|
*/
|
|
382
423
|
const thoughtBlocks = new WeakMap<object, WeakMap<ThoughtEntry, { width: number; text: string; lines: string[]; at: number }>>();
|
|
383
424
|
|
|
384
|
-
/** A live thought this long is wrapped again at most every `LONG_THOUGHT_MS` while it streams
|
|
425
|
+
/** A live thought this long is wrapped again at most every `LONG_THOUGHT_MS` while it streams; rendered as Markdown (costlier), from `LONG_THOUGHT_MARKDOWN`. */
|
|
385
426
|
const LONG_THOUGHT = 1000;
|
|
427
|
+
const LONG_THOUGHT_MARKDOWN = 300;
|
|
386
428
|
const LONG_THOUGHT_MS = 150;
|
|
387
429
|
|
|
388
430
|
function thoughtBlock(thought: ThoughtEntry, width: number, theme?: LobbyTheme): string[] {
|
|
@@ -393,28 +435,49 @@ function thoughtBlock(thought: ThoughtEntry, width: number, theme?: LobbyTheme):
|
|
|
393
435
|
}
|
|
394
436
|
const hit = cache.get(thought);
|
|
395
437
|
const now = performance.now();
|
|
396
|
-
|
|
438
|
+
const long = theme?.markdown ? LONG_THOUGHT_MARKDOWN : LONG_THOUGHT;
|
|
439
|
+
if (hit && hit.width === width && (hit.text === thought.text || (thought.live && thought.text.length > long && now - hit.at < LONG_THOUGHT_MS))) return hit.lines;
|
|
397
440
|
const lead = `${paint(theme, sourceColor(thought.source), thought.source.padEnd(SOURCE_WIDTH).slice(0, SOURCE_WIDTH))} `;
|
|
398
|
-
const lines = wrapHanging(lead, italic(theme, paint(theme, "dim", thought.text.replace(/\s*\n\s*/g, " "))), width);
|
|
441
|
+
const lines = theme?.markdown ? thoughtMarkdown(lead, thought, width, theme) : wrapHanging(lead, italic(theme, paint(theme, "dim", thought.text.replace(/\s*\n\s*/g, " "))), width);
|
|
399
442
|
cache.set(thought, { width, text: thought.text, lines, at: now });
|
|
400
443
|
return lines;
|
|
401
444
|
}
|
|
402
445
|
|
|
403
|
-
/**
|
|
404
|
-
|
|
446
|
+
/**
|
|
447
|
+
* A thought in Markdown as pi shows thinking (dimmed, in italics), after who
|
|
448
|
+
* thought it: a model's `**Checking the plan**` reads bold, not starred. The
|
|
449
|
+
* pane is small, so the blank lines between paragraphs are left out. A live
|
|
450
|
+
* thought is not kept in the renderer's cache; it changes as it streams.
|
|
451
|
+
*/
|
|
452
|
+
function thoughtMarkdown(lead: string, thought: ThoughtEntry, width: number, theme: LobbyTheme): string[] {
|
|
453
|
+
const indent = " ".repeat(textWidth(lead));
|
|
454
|
+
const body = markdownLines(thought.text, Math.max(1, width - textWidth(lead)), theme, !thought.live, "thought").filter((line) => line.trim());
|
|
455
|
+
if (body.length === 0) return [lead];
|
|
456
|
+
return body.map((line, index) => `${index === 0 ? lead : indent}${line}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** The newest `need` lines of the thoughts, oldest first: only the thoughts they reach are drawn (and back to `anchor`), and the rest estimated. */
|
|
460
|
+
export function thoughtTail(thoughts: readonly ThoughtEntry[], width: number, theme: LobbyTheme | undefined, need: number, anchor?: PaneMark): PaneLines {
|
|
405
461
|
const parts: string[][] = [];
|
|
406
462
|
let count = 0;
|
|
463
|
+
const { found, reach } = reachOf(thoughts, anchor);
|
|
464
|
+
let mark: PaneMark | undefined;
|
|
465
|
+
let grew: number | undefined;
|
|
407
466
|
let index = thoughts.length - 1;
|
|
408
|
-
for (; index >= 0 && count < need; index -= 1) {
|
|
409
|
-
const
|
|
467
|
+
for (; index >= 0 && (count < need || index >= reach); index -= 1) {
|
|
468
|
+
const thought = thoughts[index]!;
|
|
469
|
+
const lines = thoughtBlock(thought, width, theme);
|
|
410
470
|
parts.push(lines);
|
|
411
471
|
count += lines.length;
|
|
472
|
+
if (index === thoughts.length - 1) mark = { entry: thought, below: count };
|
|
473
|
+
if (index === found) grew = count - anchor!.below;
|
|
412
474
|
}
|
|
413
475
|
const lines: string[] = [];
|
|
414
476
|
for (let part = parts.length - 1; part >= 0; part -= 1) for (const line of parts[part]!) lines.push(line);
|
|
415
477
|
const rest = index + 1;
|
|
416
478
|
const drawn = parts.length;
|
|
417
|
-
|
|
479
|
+
const total = rest > 0 && drawn > 0 ? lines.length + Math.ceil((rest * lines.length) / drawn) : lines.length;
|
|
480
|
+
return { lines, total, ...(mark ? { mark } : {}), ...(grew !== undefined ? { grew } : {}) };
|
|
418
481
|
}
|
|
419
482
|
|
|
420
483
|
/** Every thought, oldest first: who thought it, then the thought, dimmed. */
|
|
@@ -422,8 +485,8 @@ export function thoughtLines(thoughts: readonly ThoughtEntry[], width: number, t
|
|
|
422
485
|
return thoughtTail(thoughts, width, theme, Number.POSITIVE_INFINITY).lines;
|
|
423
486
|
}
|
|
424
487
|
|
|
425
|
-
function thinkingContent(input: HomeInput, thoughts: readonly ThoughtEntry[], width: number, need: number, theme?: LobbyTheme): PaneLines {
|
|
426
|
-
if (thoughts.length > 0) return thoughtTail(thoughts, width, theme, need);
|
|
488
|
+
function thinkingContent(input: HomeInput, thoughts: readonly ThoughtEntry[], width: number, need: number, anchor: PaneMark | undefined, theme?: LobbyTheme): PaneLines {
|
|
489
|
+
if (thoughts.length > 0) return thoughtTail(thoughts, width, theme, need, anchor);
|
|
427
490
|
return whole([paint(theme, "dim", input.query ? `No thought matches "${input.query}".` : "Thoughts from the oracle and every agent appear here, and only here.")]);
|
|
428
491
|
}
|
|
429
492
|
|
|
@@ -482,23 +545,24 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
482
545
|
const matches = (count: number) => `${count} match${count === 1 ? "" : "es"}`;
|
|
483
546
|
/**
|
|
484
547
|
* One scrollable pane: only the lines it shows are drawn — its newest, or
|
|
485
|
-
* as far back as it is scrolled — in a box that records where it sits
|
|
548
|
+
* as far back as it is scrolled — in a box that records where it sits, and
|
|
549
|
+
* how many lines arrived below since the frame before.
|
|
486
550
|
*/
|
|
487
|
-
const pane = (name: HomePane, title: string, note: string | undefined, content: (inner: number, need: number) => PaneLines) => (top: number, left: number, w: number, h: number): string[] => {
|
|
551
|
+
const pane = (name: HomePane, title: string, note: string | undefined, content: (inner: number, need: number, anchor: PaneMark | undefined) => PaneLines) => (top: number, left: number, w: number, h: number): string[] => {
|
|
488
552
|
const rows = Math.max(0, h - 2);
|
|
489
553
|
const offset = input.offsets?.[name] ?? 0;
|
|
490
|
-
const drawn = content(Math.max(1, w - 4), rows + offset);
|
|
554
|
+
const drawn = content(Math.max(1, w - 4), rows + offset, input.anchors?.get(name));
|
|
491
555
|
const view = paneWindow(drawn, rows, offset);
|
|
492
556
|
const total = Math.max(drawn.total, drawn.lines.length);
|
|
493
|
-
notePane(input.panes, name, top, left, w, h, total);
|
|
557
|
+
notePane(input.panes, name, top, left, w, h, total, { ...(drawn.mark ? { mark: drawn.mark } : {}), grew: drawn.grew ?? 0 });
|
|
494
558
|
const right = rightNote(view.offset, note);
|
|
495
559
|
return box(w, h, view.shown, { title, ...(right ? { right } : {}), focused: input.focus === name, scroll: { total, start: view.start }, theme });
|
|
496
560
|
};
|
|
497
|
-
const chatBox = pane("conversation", input.title ? `Conversation · ${input.title}` : "Conversation", input.query ? matches(feed.chat.length) : input.keys?.conversation, (inner, need) => conversation(input, feed.chat, inner, need, theme));
|
|
498
|
-
const activityBox = pane("activity", "Activity", input.query ? matches(feed.activity.length) : input.keys?.activity, (inner, need) => activity(input, feed.activity, inner, need, theme));
|
|
561
|
+
const chatBox = pane("conversation", input.title ? `Conversation · ${input.title}` : "Conversation", input.query ? matches(feed.chat.length) : input.keys?.conversation, (inner, need, anchor) => conversation(input, feed.chat, inner, need, anchor, theme));
|
|
562
|
+
const activityBox = pane("activity", "Activity", input.query ? matches(feed.activity.length) : input.keys?.activity, (inner, need, anchor) => activity(input, feed.activity, inner, need, anchor, theme));
|
|
499
563
|
const thought = currentThought(feed.thoughts);
|
|
500
564
|
const thinkingNote = thought ? `${thought.source} · ${thought.live ? "thinking" : since(input.now - thought.at)}` : input.keys?.thinking;
|
|
501
|
-
const thinkingBox = pane("thinking", "Thinking", thinkingNote, (inner, need) => thinkingContent(input, feed.thoughts, inner, need, theme));
|
|
565
|
+
const thinkingBox = pane("thinking", "Thinking", thinkingNote, (inner, need, anchor) => thinkingContent(input, feed.thoughts, inner, need, anchor, theme));
|
|
502
566
|
const top = scene.length;
|
|
503
567
|
let body: string[] = [];
|
|
504
568
|
if (main > 0 && panels.conversation && panels.activity) {
|
package/src/lobby/tabs/issues.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* becomes a task.
|
|
5
5
|
*/
|
|
6
6
|
import type { IssueDetail, IssueSummary } from "../issues.ts";
|
|
7
|
-
import { bold, since, columns, fill, paint, rule, selectRow, spinner, split, windowStart, wrap, type LobbyTheme } from "../layout.ts";
|
|
7
|
+
import { bold, since, columns, fill, markdownLines, paint, rule, selectRow, spinner, split, windowStart, wrap, type LobbyTheme } from "../layout.ts";
|
|
8
8
|
|
|
9
9
|
export interface IssuesTabInput {
|
|
10
10
|
issues: readonly IssueSummary[];
|
|
@@ -31,9 +31,10 @@ export function issueDetailLines(issue: IssueDetail, width: number, now: number,
|
|
|
31
31
|
].filter(Boolean);
|
|
32
32
|
const lines = [...wrap(bold(theme, `#${issue.number} ${issue.title}`), width), facts.join(paint(theme, "dim", " · "))];
|
|
33
33
|
if (issue.url) lines.push(paint(theme, "dim", issue.url));
|
|
34
|
-
|
|
34
|
+
// Issues and their comments are GitHub Markdown.
|
|
35
|
+
lines.push("", paint(theme, "dim", "p plans it with the planning panel, then save it as a task"), "", ...(issue.body.trim() ? markdownLines(issue.body.trim(), width, theme) : [paint(theme, "dim", "(no description)")]));
|
|
35
36
|
for (const comment of issue.comments) {
|
|
36
|
-
lines.push("", rule(width, comment.author ?? "comment", theme, comment.createdAt ? since(now - Date.parse(comment.createdAt)) : ""), ...
|
|
37
|
+
lines.push("", rule(width, comment.author ?? "comment", theme, comment.createdAt ? since(now - Date.parse(comment.createdAt)) : ""), ...markdownLines(comment.body.trim(), width, theme));
|
|
37
38
|
}
|
|
38
39
|
return lines;
|
|
39
40
|
}
|
package/src/lobby/tabs/plan.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
10
10
|
import { ORACLE_LABEL, type LineComment, type PanelNote, type PanelQuestion, type PlannerMessage, type PlannerReply, type PlannerSeed, type RoundMode } from "../planner.ts";
|
|
11
11
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
12
|
-
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
12
|
+
import { beside, bold, box, clock, fill, fit, italic, markdownHanging, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
13
|
import { sourceColor, speakerLine, tailWindow, youLines } from "./home.ts";
|
|
14
14
|
|
|
15
15
|
export interface SeatView {
|
|
@@ -152,7 +152,7 @@ function questionLines(questions: ReadonlyArray<{ question: PanelQuestion; numbe
|
|
|
152
152
|
const lead = `${paint(theme, "dim", `${String(number).padStart(2)}.`)} ${bold(theme, paint(theme, sourceColor(question.from), question.from.padEnd(LABEL_WIDTH)))} `;
|
|
153
153
|
const indent = " ".repeat(4 + LABEL_WIDTH);
|
|
154
154
|
const options = question.options.map((option) => wrapHanging(`${indent}${paint(theme, "dim", "○")} `, `${option.label}${option.description ? paint(theme, "dim", ` — ${option.description}`) : ""}`, width)).flat();
|
|
155
|
-
return [...
|
|
155
|
+
return [...markdownHanging(lead, question.text, width, theme), ...options];
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -178,7 +178,7 @@ export function conversationLines(view: PlanView, width: number, theme?: LobbyTh
|
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
180
|
lines.push(speakerLine("panel", width, theme, time));
|
|
181
|
-
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) :
|
|
181
|
+
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) : markdownLines(message.text, Math.max(1, width - 2), theme).map((line) => (line ? ` ${line}` : ""))));
|
|
182
182
|
for (const entry of message.decided ?? []) {
|
|
183
183
|
lines.push(...wrapHanging(` ${paint(theme, "success", "✓")} `, `${paint(theme, sourceColor(entry.from), `[${entry.from}]`)} ${entry.question} → ${bold(theme, entry.answer)} ${paint(theme, "dim", `· decided by the classifier (${entry.probability.toFixed(2)}); comment on the plan to overrule`)}`, width));
|
|
184
184
|
}
|
|
@@ -210,7 +210,7 @@ export function draftLines(view: PlanView, width: number, theme?: LobbyTheme): D
|
|
|
210
210
|
}
|
|
211
211
|
if (view.notes.length > 0) {
|
|
212
212
|
lines.push(line(""), line(bold(theme, paint(theme, "mdHeading", "What each seat needs"))));
|
|
213
|
-
for (const note of view.notes) for (const wrapped of
|
|
213
|
+
for (const note of view.notes) for (const wrapped of markdownHanging(`${paint(theme, sourceColor(note.from), note.from.padEnd(LABEL_WIDTH))} `, note.text, width, theme)) lines.push(line(wrapped));
|
|
214
214
|
}
|
|
215
215
|
return lines;
|
|
216
216
|
}
|
|
@@ -61,15 +61,21 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
61
61
|
job.tools ? `${job.tools} tool${job.tools === 1 ? "" : "s"}` : "",
|
|
62
62
|
job.usage?.cost ? `$${job.usage.cost.toFixed(2)}` : "",
|
|
63
63
|
].filter(Boolean);
|
|
64
|
-
|
|
64
|
+
// What you asked, in Markdown as your messages read in the conversation (a pasted snippet, a list of steps).
|
|
65
|
+
const asked = theme?.markdown ? markdownLines(job.prompt, width, theme, true, "you") : wrap(bold(theme, job.prompt), width);
|
|
66
|
+
const lines = [paint(theme, "dim", job.id), facts.join(paint(theme, "dim", " · ")), "", ...asked];
|
|
65
67
|
if (job.route) lines.push("", ...wrap(paint(theme, "dim", `↓ routed ${job.route}`), width));
|
|
66
|
-
if (job.note) lines.push("", ...wrap(paint(theme, "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
68
|
+
if (job.note) lines.push("", ...wrap(paint(theme, job.status === "held" ? "warning" : job.routed ? "muted" : "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
67
69
|
lines.push("", rule(width, "Steps", theme));
|
|
68
70
|
if (job.steps.length === 0) lines.push(paint(theme, "dim", job.status === "queued" ? "Waiting for the quick fix ahead of it." : job.status === "running" ? `${spinner(tick)} starting…` : job.status === "held" ? "Not started." : "No tool calls."));
|
|
69
71
|
for (const step of job.steps) {
|
|
70
72
|
const lead = `${paint(theme, "dim", clock(step.at))} ${step.pending && job.status === "running" ? paint(theme, "accent", spinner(tick)) : paint(theme, "dim", "·")} `;
|
|
71
73
|
lines.push(...wrapHanging(lead, step.pending && job.status === "running" ? `${step.text}…` : paint(theme, "muted", step.text), width));
|
|
72
74
|
}
|
|
75
|
+
if (job.files?.length) {
|
|
76
|
+
lines.push("", rule(width, "Edited", theme));
|
|
77
|
+
for (const file of job.files) lines.push(...wrapHanging(paint(theme, "dim", "· "), paint(theme, "muted", file), width));
|
|
78
|
+
}
|
|
73
79
|
if (job.error) lines.push("", ...wrap(paint(theme, "error", `✗ ${job.error}`), width));
|
|
74
80
|
if (job.report) {
|
|
75
81
|
lines.push("", rule(width, "Report", theme), ...markdownLines(job.report, width, theme));
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { planChecklist } from "../../pi/zen.ts";
|
|
|
11
11
|
import { persistedRuns } from "../../pi/ui.ts";
|
|
12
12
|
import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { textWidth } from "../../width.ts";
|
|
15
|
+
import { ago, beside, bold, box, detailWindow, fill, markdownHanging, markdownLines, notePane, paint, position, rule, selectRow, since, spread, strike, windowStart, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
15
16
|
|
|
16
17
|
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
17
18
|
|
|
@@ -240,6 +241,13 @@ export function listCount(rows: readonly TaskRow[], query?: string): string {
|
|
|
240
241
|
const COMMENT_MARKS: Record<PlanComment["status"], string> = { open: "○", delivered: "◐", addressed: "✓" };
|
|
241
242
|
const COMMENT_WORDS: Record<PlanComment["status"], string> = { open: "waiting for the owning session", delivered: "sent to the oracle", addressed: "plan amended" };
|
|
242
243
|
|
|
244
|
+
/** `note` at the end of the last line when it fits there, else on a line of its own under it. */
|
|
245
|
+
function trailing(lines: string[], note: string, width: number): string[] {
|
|
246
|
+
const last = lines.at(-1) ?? "";
|
|
247
|
+
if (textWidth(last) + 1 + textWidth(note) <= width) return [...lines.slice(0, -1), `${last} ${note}`];
|
|
248
|
+
return [...lines, ...wrapHanging(" ", note, width)];
|
|
249
|
+
}
|
|
250
|
+
|
|
243
251
|
function section(title: string, width: number, theme?: LobbyTheme, right = ""): string[] {
|
|
244
252
|
return ["", rule(width, title, theme, right)];
|
|
245
253
|
}
|
|
@@ -260,7 +268,8 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
260
268
|
finished ? paint(theme, "muted", `${task.state === "abandoned" ? "dropped" : "done"} ${since(now - Date.parse(task.updatedAt))}`) : "",
|
|
261
269
|
].filter(Boolean);
|
|
262
270
|
lines.push(...wrapHanging(" ", facts.join(dot), width));
|
|
263
|
-
|
|
271
|
+
const track = task.track ? (task.track.path === "fast" ? `fast track (${task.track.size})` : `full workflow (${task.track.size})`) : "";
|
|
272
|
+
lines.push(...wrapHanging(" ", paint(theme, "dim", [task.id, track, task.domains.length > 0 ? task.domains.join(", ") : ""].filter(Boolean).join(" · ")), width));
|
|
264
273
|
const steps = task.plan ? planChecklist(task.plan, persistedRuns(task)) : [];
|
|
265
274
|
const done = steps.filter((step) => step.status === "done").length;
|
|
266
275
|
if (steps.length > 0) {
|
|
@@ -268,7 +277,8 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
268
277
|
lines.push("", ` ${pips(done, steps.length, cells, theme)} ${paint(theme, "muted", `${done} of ${steps.length} steps`)}`);
|
|
269
278
|
}
|
|
270
279
|
const request = taskRequest(task);
|
|
271
|
-
|
|
280
|
+
// Requests are often Markdown: a plan agreed in the planning panel, an issue, a pasted spec.
|
|
281
|
+
if (request && request !== task.title) lines.push(...section("Request", width, theme), ...markdownLines(request, width, theme));
|
|
272
282
|
if (task.plan) {
|
|
273
283
|
if (steps.length > 0) {
|
|
274
284
|
lines.push(...section("Progress", width, theme, `${done}/${steps.length} steps`));
|
|
@@ -291,11 +301,12 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
291
301
|
if (comments.length === 0) lines.push(paint(theme, "dim", TERMINAL_STATES.includes(task.state) ? "No comments." : "No comments yet — press c to comment on the plan; the oracle amends it."));
|
|
292
302
|
for (const comment of comments) {
|
|
293
303
|
const color = comment.status === "addressed" ? "success" : comment.status === "delivered" ? "accent" : "warning";
|
|
294
|
-
|
|
304
|
+
const said = markdownHanging(`${paint(theme, color, COMMENT_MARKS[comment.status])} `, comment.text, width, theme);
|
|
305
|
+
lines.push(...trailing(said, paint(theme, "dim", `— ${COMMENT_WORDS[comment.status]}, ${since(now - Date.parse(comment.createdAt))}`), width));
|
|
295
306
|
}
|
|
296
307
|
if (task.amendments.length > 0) {
|
|
297
308
|
lines.push(...section("Amendments", width, theme));
|
|
298
|
-
for (const amendment of task.amendments) lines.push(...
|
|
309
|
+
for (const amendment of task.amendments) lines.push(...markdownHanging("- ", amendment, width, theme));
|
|
299
310
|
}
|
|
300
311
|
const approvals = pendingApprovals(task);
|
|
301
312
|
if (approvals.length > 0 || task.blockers.length > 0) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's theme as bot-lobby draws with it (the lobby, the questionnaire), plus
|
|
3
|
+
* Markdown rendering; one wrapper (and one Markdown renderer with its cache)
|
|
4
|
+
* per theme, so caches stay warm and a theme switch starts fresh.
|
|
5
|
+
*/
|
|
6
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { LobbyTheme } from "./layout.ts";
|
|
8
|
+
import { createMarkdownRenderer } from "./markdown.ts";
|
|
9
|
+
|
|
10
|
+
const lobbyThemes = new WeakMap<Theme, LobbyTheme>();
|
|
11
|
+
|
|
12
|
+
export function lobbyTheme(theme: Theme): LobbyTheme {
|
|
13
|
+
let wrapped = lobbyThemes.get(theme);
|
|
14
|
+
if (!wrapped) {
|
|
15
|
+
wrapped = {
|
|
16
|
+
fg: (color, text) => theme.fg(color, text),
|
|
17
|
+
bold: (text) => theme.bold(text),
|
|
18
|
+
italic: (text) => theme.italic(text),
|
|
19
|
+
bg: (color, text) => theme.bg(color, text),
|
|
20
|
+
// The same looks pi gives your messages and the thinking it shows.
|
|
21
|
+
markdown: createMarkdownRenderer(getMarkdownTheme(), {
|
|
22
|
+
you: { color: (text) => theme.fg("accent", text) },
|
|
23
|
+
thought: { color: (text) => theme.fg("thinkingText", text), italic: true },
|
|
24
|
+
}),
|
|
25
|
+
strike: (text) => theme.strikethrough(text),
|
|
26
|
+
};
|
|
27
|
+
lobbyThemes.set(theme, wrapped);
|
|
28
|
+
}
|
|
29
|
+
return wrapped;
|
|
30
|
+
}
|
package/src/lobby/view.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { QuickFixQueue } from "./quickfix.ts";
|
|
|
23
23
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
24
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
25
25
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
26
|
-
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
+
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
27
27
|
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
28
28
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
29
29
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
@@ -320,6 +320,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
320
320
|
homeFocus: HomePane = "conversation";
|
|
321
321
|
/** How many lines each pane held last frame, so a pane scrolled back stays on what you are reading as lines arrive. */
|
|
322
322
|
private readonly seenTotals = new Map<string, number>();
|
|
323
|
+
/** Each Lobby pane's newest entry last frame: those panes measure what arrived below it, as their totals are estimates. */
|
|
324
|
+
private readonly marks = new Map<string, PaneMark>();
|
|
323
325
|
/** Where the current tab's scrollable panes landed in the last frame. */
|
|
324
326
|
private readonly panes: PaneLayout = new Map();
|
|
325
327
|
private tasksSelected = 0;
|
|
@@ -469,6 +471,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
469
471
|
return visibleTabs(this.issuesOn);
|
|
470
472
|
}
|
|
471
473
|
|
|
474
|
+
/** Open the Quick fix tab on a job the oracle routed there, with a line saying so. */
|
|
475
|
+
showQuickFix(id: string): void {
|
|
476
|
+
this.setTab("quickfix");
|
|
477
|
+
delete this.queries.quickfix;
|
|
478
|
+
this.fixSelected = Math.max(0, this.fixJobs().findIndex((job) => job.id === id));
|
|
479
|
+
this.fixDetailOffset = 0;
|
|
480
|
+
this.say(`${id} — the oracle sent your request to the quick-fix agent`);
|
|
481
|
+
}
|
|
482
|
+
|
|
472
483
|
setTab(tab: TabId): void {
|
|
473
484
|
if (tab === this.tab) return;
|
|
474
485
|
if (!this.tabs().includes(tab)) return this.say(`the ${TAB_LABELS[tab]} tab is off — set lobby.${tab} to true in the config to bring it back`, "warning");
|
|
@@ -894,11 +905,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
894
905
|
const limit = (name: string, offset: number, anchored: boolean): number => {
|
|
895
906
|
const pane = this.panes.get(name);
|
|
896
907
|
if (!pane) return offset;
|
|
897
|
-
const
|
|
898
|
-
this.seenTotals.set(name, pane.total);
|
|
908
|
+
const grew = this.grewBelow(name, pane);
|
|
899
909
|
let next = offset;
|
|
900
|
-
if (anchored && next > 0 &&
|
|
901
|
-
next +=
|
|
910
|
+
if (anchored && next > 0 && grew > 0) {
|
|
911
|
+
next += grew;
|
|
902
912
|
moved = true;
|
|
903
913
|
}
|
|
904
914
|
return Math.min(next, Math.max(0, pane.total - pane.rows));
|
|
@@ -922,6 +932,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
922
932
|
return moved;
|
|
923
933
|
}
|
|
924
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Lines that arrived below a pane since it was last looked at, counted once.
|
|
937
|
+
* A pane that measures them from its newest entry says so; for the others
|
|
938
|
+
* (their totals exact) the total's growth stands in. An estimated total also
|
|
939
|
+
* moves as scrolling draws more or fewer of the entries it estimates, and
|
|
940
|
+
* following that pinned a pane scrolled to its top there.
|
|
941
|
+
*/
|
|
942
|
+
private grewBelow(name: string, pane: PaneBox): number {
|
|
943
|
+
const seen = this.seenTotals.get(name);
|
|
944
|
+
this.seenTotals.set(name, pane.total);
|
|
945
|
+
const follow = pane.follow;
|
|
946
|
+
if (!follow) return seen === undefined ? 0 : pane.total - seen;
|
|
947
|
+
if (follow.mark) this.marks.set(name, follow.mark);
|
|
948
|
+
else this.marks.delete(name);
|
|
949
|
+
const grew = follow.grew;
|
|
950
|
+
follow.grew = 0;
|
|
951
|
+
return grew;
|
|
952
|
+
}
|
|
953
|
+
|
|
925
954
|
/** Move the draft cursor by `delta` lines, skipping blank ones, and keep it on screen. */
|
|
926
955
|
private moveCursor(delta: number): void {
|
|
927
956
|
const lines = this.planLayout.draftText;
|
|
@@ -2276,6 +2305,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2276
2305
|
others,
|
|
2277
2306
|
pending: this.data.plans.filter((plan) => plan.status === "pending").length,
|
|
2278
2307
|
offsets: this.homeOffsets,
|
|
2308
|
+
anchors: this.marks,
|
|
2279
2309
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2280
2310
|
panes: this.panes,
|
|
2281
2311
|
tick: this.tick,
|
|
@@ -2306,6 +2336,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2306
2336
|
others: 0,
|
|
2307
2337
|
pending: 0,
|
|
2308
2338
|
offsets: this.homeOffsets,
|
|
2339
|
+
anchors: this.marks,
|
|
2309
2340
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2310
2341
|
panes: this.panes,
|
|
2311
2342
|
tick: this.tick,
|
package/src/master/decisions.ts
CHANGED
|
@@ -2,12 +2,20 @@ import type { Decision, Task, Verdict } from "../schemas/task.ts";
|
|
|
2
2
|
import type { Domain } from "../schemas/agent.ts";
|
|
3
3
|
import type { ScoutOutcome } from "./master.ts";
|
|
4
4
|
import { detectGaps, domainsInvolved } from "./synthesis.ts";
|
|
5
|
+
import { builtSomething, onFastTrack, qaRequired, qaTookPart } from "../workflow/track.ts";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* §19: everything the engine requires before completion may be declared. The
|
|
9
|
+
* fast track asks for a finished worker step instead of a QA pass, and for
|
|
10
|
+
* QA's part only when the change needs tests.
|
|
11
|
+
*/
|
|
7
12
|
export function completionBlockers(task: Task, pendingCount: number): string[] {
|
|
8
13
|
const blockers: string[] = [];
|
|
9
14
|
if (!task.plan) blockers.push("no approved plan is recorded");
|
|
10
|
-
if (task
|
|
15
|
+
if (onFastTrack(task)) {
|
|
16
|
+
if (!builtSomething(task)) blockers.push("no worker step has finished yet");
|
|
17
|
+
if (qaRequired(task) && !qaTookPart(task) && !task.qaWaiver) blockers.push("QA has not taken part (give qa the tests as the last step, or run the QA gate)");
|
|
18
|
+
} else if (task.qaVerdict !== "pass" && !task.qaWaiver) blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
|
|
11
19
|
if (pendingCount > 0) blockers.push(`${pendingCount} unresolved approval request(s)`);
|
|
12
20
|
if (task.blockers.length > 0) blockers.push(`${task.blockers.length} unresolved blocker(s)`);
|
|
13
21
|
return blockers;
|