@a-t-h-i/bot-lobby 0.5.1 → 0.6.1
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 -44
- package/package.json +1 -1
- package/prompts/panel.md +6 -2
- package/prompts/planner.md +29 -16
- package/src/execution/pi-runner.ts +1 -1
- package/src/index.ts +4 -2
- package/src/lobby/feed.ts +65 -9
- package/src/lobby/keys.ts +5 -1
- package/src/lobby/layout.ts +17 -2
- package/src/lobby/planner.ts +16 -5
- package/src/lobby/runtime.ts +317 -61
- package/src/lobby/session-files.ts +138 -0
- package/src/lobby/sessions.ts +314 -0
- package/src/lobby/tabs/home.ts +145 -29
- package/src/lobby/tabs/plan.ts +25 -30
- package/src/lobby/tabs/quickfix.ts +5 -8
- package/src/lobby/tabs/tasks.ts +200 -51
- package/src/lobby/view.ts +763 -89
- package/src/pi/commands.ts +52 -6
- package/src/pi/events.ts +5 -2
- package/src/pi/notify.ts +18 -0
- package/src/pi/owner.ts +265 -0
- package/src/pi/settings-ui.ts +6 -2
- package/src/pi/start-task.ts +33 -2
- package/src/pi/ui.ts +3 -3
- package/src/pi/zen-large.ts +9 -1
- package/src/pi/zen.ts +7 -3
- package/src/schemas/configuration.ts +8 -3
- package/src/schemas/task.ts +4 -0
- package/src/state/archive.ts +99 -0
- package/src/state/auto.ts +40 -0
- package/src/state/inbox.ts +124 -0
- package/src/state/presence.ts +99 -0
- package/src/workflow/workflow.ts +27 -3
package/src/lobby/tabs/home.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Lobby tab: the zen scene of this session's task on top
|
|
2
|
+
* The Lobby tab: the zen scene of this session's task on top (the oracle
|
|
3
|
+
* and agent animations, or just their status when animations are off), then the
|
|
3
4
|
* conversation with the oracle (text only, no tool rows, no thinking), the
|
|
4
5
|
* activity log of plain-words steps from every agent, and the one place where
|
|
5
6
|
* thoughts show up. Pure: the scene arrives as a callback, the clock as `now`.
|
|
6
7
|
*/
|
|
8
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
7
9
|
import type { Task } from "../../schemas/task.ts";
|
|
8
10
|
import type { ActivityEntry, ChatEntry, ThoughtEntry } from "../feed.ts";
|
|
9
11
|
import type { LobbyPanel } from "../../schemas/configuration.ts";
|
|
10
|
-
import { beside, bold, box, clock, fill, italic,
|
|
12
|
+
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, since, spinner, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
11
13
|
|
|
12
14
|
/** The Lobby tab's scrollable panes. */
|
|
13
15
|
export const HOME_PANES = ["conversation", "activity", "thinking"] as const;
|
|
@@ -15,8 +17,8 @@ export type HomePane = (typeof HOME_PANES)[number];
|
|
|
15
17
|
|
|
16
18
|
export interface HomeInput {
|
|
17
19
|
task?: Task;
|
|
18
|
-
/** Draws the zen scene into at most `height` lines; absent when no task is active. */
|
|
19
|
-
scene?: (width: number, height: number) => string[];
|
|
20
|
+
/** Draws the zen scene into at most `height` lines, animated or still; absent when no task is active. */
|
|
21
|
+
scene?: (width: number, height: number, animated: boolean) => string[];
|
|
20
22
|
chat: readonly ChatEntry[];
|
|
21
23
|
/** The oracle's reply while it streams. */
|
|
22
24
|
liveReply?: string;
|
|
@@ -41,13 +43,22 @@ export interface HomeInput {
|
|
|
41
43
|
query?: string;
|
|
42
44
|
/** Key labels for the pane toggles, shown on the panes. */
|
|
43
45
|
keys?: Record<LobbyPanel, string>;
|
|
46
|
+
/** Another session is shown: its name titles the conversation. */
|
|
47
|
+
title?: string;
|
|
48
|
+
/** The scene is another session's still status (no animations to toggle). */
|
|
49
|
+
stillScene?: boolean;
|
|
50
|
+
/** What the empty conversation says instead of the default (a session starting, say). */
|
|
51
|
+
emptyNote?: string;
|
|
52
|
+
/** What the empty activity log says instead of "No activity yet.". */
|
|
53
|
+
activityNote?: string;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
/** Wide terminals put the conversation and the activity log side by side. */
|
|
47
57
|
export const HOME_COLUMNS_MIN = 100;
|
|
48
|
-
/** Most lines the scene may take, and its share of the body. */
|
|
58
|
+
/** Most lines the scene may take, and its share of the body; the still status needs far fewer. */
|
|
49
59
|
const SCENE_SHARE = 0.45;
|
|
50
60
|
const MAX_SCENE = 36;
|
|
61
|
+
const MAX_STILL = 10;
|
|
51
62
|
|
|
52
63
|
const SOURCE_COLORS: Record<string, LobbyColor> = {
|
|
53
64
|
MASTER: "accent",
|
|
@@ -83,34 +94,121 @@ export function activityLine(entry: ActivityEntry, width: number, tick: number,
|
|
|
83
94
|
return `${paint(theme, "dim", clock(entry.at))} ${who} ${mark} ${body}`;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
/**
|
|
98
|
+
* How each speaker is shown: a mark and a name in its colour, and the side of
|
|
99
|
+
* the conversation it speaks from — you on the right, the oracle on the left.
|
|
100
|
+
*/
|
|
101
|
+
export const SPEAKERS = {
|
|
102
|
+
you: { mark: "●", name: "You", color: "accent", side: "right" },
|
|
103
|
+
oracle: { mark: "◆", name: "Oracle", color: "toolTitle", side: "left" },
|
|
104
|
+
panel: { mark: "◆", name: "Panel", color: "toolTitle", side: "left" },
|
|
105
|
+
} as const satisfies Record<string, { mark: string; name: string; color: LobbyColor; side: "left" | "right" }>;
|
|
106
|
+
export type Speaker = keyof typeof SPEAKERS;
|
|
107
|
+
|
|
108
|
+
/** Messages from one speaker within this long of each other share a header. */
|
|
109
|
+
const GROUP_MS = 5 * 60_000;
|
|
110
|
+
/** The oracle's replies sit under its name, past its mark. */
|
|
111
|
+
const BODY_INDENT = 2;
|
|
112
|
+
/** Your messages take at most this share of the pane, the oracle's this much, so the two sides read apart. */
|
|
113
|
+
const YOU_SHARE = 0.72;
|
|
114
|
+
const ORACLE_SHARE = 0.86;
|
|
115
|
+
/** Narrower panes give every message (nearly) the full width. */
|
|
116
|
+
const NARROW = 48;
|
|
117
|
+
|
|
118
|
+
/** The widest a message from one side may be in a pane `width` wide. */
|
|
119
|
+
function messageWidth(width: number, share: number, margin: number): number {
|
|
120
|
+
return Math.max(1, width < NARROW ? width - margin : Math.floor(width * share));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Who speaks and when: `◆ Oracle ········ 12:04` on the left for the oracle,
|
|
125
|
+
* `12:04 You ●` on the right for you. `note` is the time, or what the
|
|
126
|
+
* speaker is doing (`⠋ writing`).
|
|
127
|
+
*/
|
|
128
|
+
export function speakerLine(speaker: Speaker, width: number, theme?: LobbyTheme, note = ""): string {
|
|
129
|
+
const { mark, name, color, side } = SPEAKERS[speaker];
|
|
130
|
+
const who = paint(theme, color, mark);
|
|
131
|
+
const label = bold(theme, paint(theme, color, name));
|
|
132
|
+
const dimmed = note ? paint(theme, "dim", note) : "";
|
|
133
|
+
if (side === "right") {
|
|
134
|
+
const head = dimmed ? `${dimmed} ${label} ${who}` : `${label} ${who}`;
|
|
135
|
+
return `${" ".repeat(Math.max(0, width - visibleWidth(head)))}${head}`;
|
|
136
|
+
}
|
|
137
|
+
const left = `${who} ${label}`;
|
|
138
|
+
const gap = width - visibleWidth(left) - visibleWidth(dimmed);
|
|
139
|
+
return gap >= 1 && dimmed ? `${left}${" ".repeat(gap)}${dimmed}` : left;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What you wrote: a bubble on the right, only as wide as its longest line (up
|
|
144
|
+
* to `YOU_SHARE` of the pane), your words in the accent colour on pi's
|
|
145
|
+
* user-message background — or, where the theme has no background, closed
|
|
146
|
+
* by a bar in that colour.
|
|
147
|
+
*/
|
|
148
|
+
export function youLines(text: string, width: number, theme?: LobbyTheme): string[] {
|
|
149
|
+
const most = Math.max(1, messageWidth(width, YOU_SHARE, 4) - 2);
|
|
150
|
+
const lines = wrap(text, most);
|
|
151
|
+
const inner = Math.max(1, ...lines.map((line) => visibleWidth(line)));
|
|
152
|
+
const pad = " ".repeat(Math.max(0, width - inner - 2));
|
|
153
|
+
const bubble = theme?.bg
|
|
154
|
+
? (line: string) => theme.bg!("userMessageBg", ` ${fit(paint(theme, "accent", line), inner)} `)
|
|
155
|
+
: (line: string) => `${fit(paint(theme, "accent", line), inner)} ${paint(theme, "accent", "▐")}`;
|
|
156
|
+
return lines.map((line) => `${pad}${bubble(line)}`);
|
|
90
157
|
}
|
|
91
158
|
|
|
92
|
-
/** The
|
|
159
|
+
/** The oracle's reply as Markdown on the left, under its name. */
|
|
160
|
+
function oracleLines(text: string, width: number, theme?: LobbyTheme): string[] {
|
|
161
|
+
const indent = " ".repeat(BODY_INDENT);
|
|
162
|
+
return markdownLines(text, Math.max(1, messageWidth(width, ORACLE_SHARE, 0) - BODY_INDENT), theme).map((line) => (line ? `${indent}${line}` : ""));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An event in the conversation (a task starting, a comment sent) as a centred rule; failures stand out instead. */
|
|
166
|
+
export function eventLines(text: string, at: number, width: number, theme?: LobbyTheme): string[] {
|
|
167
|
+
if (text.startsWith("✗")) return wrap(paint(theme, "error", text), width);
|
|
168
|
+
const label = ` ${text}${at > 0 ? ` · ${clock(at)}` : ""} `;
|
|
169
|
+
const room = width - visibleWidth(label);
|
|
170
|
+
if (room < 6) return wrap(paint(theme, "dim", text), width);
|
|
171
|
+
const left = Math.floor(room / 2);
|
|
172
|
+
return [paint(theme, "dim", `${"─".repeat(left)}${label}${"─".repeat(room - left)}`)];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The conversation, oldest first, like a chat: your messages as bubbles on
|
|
177
|
+
* the right, the oracle's replies in Markdown on the left, each turn under a
|
|
178
|
+
* line naming its speaker and time, events as centred rules, and while the
|
|
179
|
+
* oracle works its header says so (streaming the reply under it).
|
|
180
|
+
*/
|
|
93
181
|
export function chatLines(chat: readonly ChatEntry[], width: number, theme?: LobbyTheme, live?: string, busy = false, tick = 0): string[] {
|
|
94
182
|
const lines: string[] = [];
|
|
95
|
-
const
|
|
96
|
-
if (lines.length > 0) lines.push("");
|
|
97
|
-
// The oracle writes Markdown; what you typed and notes are shown as written.
|
|
98
|
-
if (role === "oracle") lines.push(...markdownHanging(chatLead(role, theme), text, width, theme));
|
|
99
|
-
else lines.push(...wrapHanging(chatLead(role, theme), role === "note" ? paint(theme, "dim", text) : text, width));
|
|
183
|
+
const gap = () => {
|
|
184
|
+
if (lines.length > 0 && lines.at(-1) !== "") lines.push("");
|
|
100
185
|
};
|
|
101
|
-
|
|
186
|
+
let last: ChatEntry | undefined;
|
|
187
|
+
for (const entry of chat) {
|
|
188
|
+
gap();
|
|
189
|
+
if (entry.role === "note") {
|
|
190
|
+
lines.push(...eventLines(entry.text, entry.at, width, theme));
|
|
191
|
+
last = undefined;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const grouped = last?.role === entry.role && entry.at >= last.at && entry.at - last.at < GROUP_MS;
|
|
195
|
+
if (!grouped) lines.push(speakerLine(entry.role, width, theme, entry.at > 0 ? clock(entry.at) : ""));
|
|
196
|
+
lines.push(...(entry.role === "you" ? youLines(entry.text, width, theme) : oracleLines(entry.text, width, theme)));
|
|
197
|
+
last = entry;
|
|
198
|
+
}
|
|
102
199
|
if (live?.trim()) {
|
|
103
|
-
|
|
104
|
-
lines
|
|
200
|
+
gap();
|
|
201
|
+
lines.push(speakerLine("oracle", width, theme, `${spinner(tick)} writing`), ...oracleLines(live.trim(), width, theme));
|
|
105
202
|
} else if (busy) {
|
|
106
|
-
|
|
107
|
-
lines.push(
|
|
203
|
+
gap();
|
|
204
|
+
lines.push(speakerLine("oracle", width, theme, `${spinner(tick)} working…`));
|
|
108
205
|
}
|
|
109
206
|
return lines;
|
|
110
207
|
}
|
|
111
208
|
|
|
112
209
|
function emptyChat(input: HomeInput, width: number, theme?: LobbyTheme): string[] {
|
|
113
210
|
if (input.query) return wrap(paint(theme, "dim", `Nothing in the conversation matches "${input.query}".`), width);
|
|
211
|
+
if (input.emptyNote) return wrap(paint(theme, "dim", input.emptyNote), width);
|
|
114
212
|
const lines = input.task
|
|
115
213
|
? [paint(theme, "dim", "Nothing said yet. Type below to talk to the oracle about this task.")]
|
|
116
214
|
: [
|
|
@@ -147,7 +245,7 @@ function conversation(input: HomeInput, chat: readonly ChatEntry[], width: numbe
|
|
|
147
245
|
}
|
|
148
246
|
|
|
149
247
|
function activity(input: HomeInput, entries: readonly ActivityEntry[], width: number, theme?: LobbyTheme): string[] {
|
|
150
|
-
if (entries.length === 0) return
|
|
248
|
+
if (entries.length === 0) return wrap(paint(theme, "dim", input.query ? `No activity matches "${input.query}".` : input.activityNote ?? "No activity yet."), width);
|
|
151
249
|
return entries.map((entry) => activityLine(entry, width, input.tick, theme));
|
|
152
250
|
}
|
|
153
251
|
|
|
@@ -177,17 +275,35 @@ function thinkingContent(input: HomeInput, thoughts: readonly ThoughtEntry[], wi
|
|
|
177
275
|
return [paint(theme, "dim", input.query ? `No thought matches "${input.query}".` : "Thoughts from the oracle and every agent appear here, and only here.")];
|
|
178
276
|
}
|
|
179
277
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
278
|
+
/**
|
|
279
|
+
* The scene: with animations off (the `animations` panel, off by default)
|
|
280
|
+
* only the task's status box, what the agents are doing and the checklist;
|
|
281
|
+
* with them on, the animated oracle and agents too. Its first line names the
|
|
282
|
+
* key that toggles the animations.
|
|
283
|
+
*/
|
|
284
|
+
function sceneLines(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
285
|
+
if (!input.task || !input.scene) return [];
|
|
286
|
+
const animated = input.panels.animations && !input.stillScene;
|
|
287
|
+
const budget = Math.min(animated ? MAX_SCENE : MAX_STILL, Math.floor(height * SCENE_SHARE));
|
|
288
|
+
if (budget < (animated ? 6 : 4)) return [];
|
|
289
|
+
const lines = input.scene(width, budget, animated).slice(0, budget);
|
|
290
|
+
const note = input.keys && !input.stillScene ? `${input.keys.animations} ${animated ? "hides" : "shows"} animations` : undefined;
|
|
291
|
+
return keyNote(lines, width, note, theme);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** `note` at the right end of the first line, dimmed, when it fits beside what is there. */
|
|
295
|
+
function keyNote(lines: readonly string[], width: number, note: string | undefined, theme?: LobbyTheme): string[] {
|
|
296
|
+
const first = lines[0];
|
|
297
|
+
if (!note || first === undefined) return [...lines];
|
|
298
|
+
const gap = width - visibleWidth(first) - visibleWidth(note) - 1;
|
|
299
|
+
if (gap < 2) return [...lines];
|
|
300
|
+
return [`${first}${" ".repeat(gap)}${paint(theme, "dim", note)}`, ...lines.slice(1)];
|
|
185
301
|
}
|
|
186
302
|
|
|
187
303
|
function hiddenHint(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
188
304
|
const keys = input.keys;
|
|
189
305
|
const text = keys
|
|
190
|
-
? `Every pane is hidden — ${keys.conversation} conversation · ${keys.activity} activity · ${keys.thinking} thinking
|
|
306
|
+
? `Every pane is hidden — ${keys.conversation} conversation · ${keys.activity} activity · ${keys.thinking} thinking`
|
|
191
307
|
: "Every pane is hidden.";
|
|
192
308
|
const lines = wrap(paint(theme, "dim", text), width);
|
|
193
309
|
const top = Math.max(0, Math.floor((height - lines.length) / 2));
|
|
@@ -205,7 +321,7 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
205
321
|
if (height <= 0) return [];
|
|
206
322
|
const { panels } = input;
|
|
207
323
|
const feed = filterFeed(input);
|
|
208
|
-
const scene = sceneLines(input, width, height);
|
|
324
|
+
const scene = sceneLines(input, width, height, theme);
|
|
209
325
|
const rest = height - scene.length;
|
|
210
326
|
const showMain = panels.conversation || panels.activity;
|
|
211
327
|
const thinkHeight = !panels.thinking ? 0 : !showMain ? rest : rest >= 18 ? Math.max(5, Math.floor(rest * 0.25)) : rest >= 10 ? 4 : 0;
|
|
@@ -220,7 +336,7 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
220
336
|
const right = rightNote(view.offset, note);
|
|
221
337
|
return box(w, h, view.shown, { title, ...(right ? { right } : {}), focused: input.focus === name, scroll: { total: all.length, start: view.start }, theme });
|
|
222
338
|
};
|
|
223
|
-
const chatBox = pane("conversation", input.
|
|
339
|
+
const chatBox = pane("conversation", input.title ? `Conversation · ${input.title}` : "Conversation", input.query ? matches(feed.chat.length) : input.keys?.conversation, (inner) => conversation(input, feed.chat, inner, theme));
|
|
224
340
|
const activityBox = pane("activity", "Activity", input.query ? matches(feed.activity.length) : input.keys?.activity, (inner) => activity(input, feed.activity, inner, theme));
|
|
225
341
|
const thought = currentThought(feed.thoughts);
|
|
226
342
|
const thinkingNote = thought ? `${thought.source} · ${thought.live ? "thinking" : since(input.now - thought.at)}` : input.keys?.thinking;
|
package/src/lobby/tabs/plan.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
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 } from "../planner.ts";
|
|
11
11
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
12
|
-
import { beside, bold, box, fill, italic, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
|
-
import { sourceColor, tailWindow } from "./home.ts";
|
|
12
|
+
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
|
+
import { sourceColor, speakerLine, tailWindow, youLines } from "./home.ts";
|
|
14
14
|
|
|
15
15
|
export interface SeatView {
|
|
16
16
|
label: string;
|
|
@@ -77,43 +77,34 @@ export interface PlanTabInput {
|
|
|
77
77
|
layout?: PlanLayout;
|
|
78
78
|
/** Filled with where the conversation (`talk`) and the draft landed, for scrolling. */
|
|
79
79
|
panes?: PaneLayout;
|
|
80
|
+
/** The key that saves the plan, as the status line names it. */
|
|
81
|
+
saveKey?: string;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export const PLAN_COLUMNS_MIN = 100;
|
|
83
85
|
const LABEL_WIDTH = 8;
|
|
84
86
|
|
|
87
|
+
/** Before a session: one sentence, then who sits on the panel and the model each runs on. */
|
|
85
88
|
function intro(input: PlanTabInput, width: number, theme?: LobbyTheme): string[] {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`Describe what you want below. The planning panel — the oracle chairing${seats.length > 0 ? `, with ${seats.join(", ")}` : ""} — reads the codebase and questions you, each seat from its own domain: contracts and data, flows and states, acceptance criteria and tests, libraries and prior art.`,
|
|
93
|
-
"",
|
|
94
|
-
"After each round the oracle puts the panel's questions to you one at a time, each with options and room for your own answer, and folds every answer into the draft plan, so all the agents start from the same decisions.",
|
|
95
|
-
"",
|
|
96
|
-
"Comment on any line of the draft: enter moves to the draft, ↑↓ pick a line (or click it), c comments. When the panel agrees, s saves the plan to the pending tasks list. While browsing (esc), 1-4 seat or unseat DEV, DESIGN, QA and RESEARCH.",
|
|
97
|
-
"",
|
|
98
|
-
paint(theme, "dim", `oracle: ${input.profile} (m changes it; each seat uses its domain's settings, alt+s)`),
|
|
99
|
-
...members.map((seat) => paint(theme, "dim", `${seat.label.toLowerCase()}: ${seat.seated ? seat.profile ?? "" : "not seated"}`)),
|
|
100
|
-
];
|
|
101
|
-
return text.flatMap((line) => wrap(line, width));
|
|
89
|
+
const rows = input.seats.map((seat) => {
|
|
90
|
+
const name = paint(theme, seat.seated ? sourceColor(seat.label) : "dim", seat.label.padEnd(LABEL_WIDTH + 2));
|
|
91
|
+
const model = seat.label === ORACLE_LABEL ? input.profile : seat.seated ? seat.profile ?? "" : "not seated";
|
|
92
|
+
return fit(`${name}${paint(theme, seat.seated ? "muted" : "dim", model)}`, width);
|
|
93
|
+
});
|
|
94
|
+
return [...wrap(bold(theme, "Describe a task below and the panel questions you until the plan is clear."), width), "", ...rows];
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
function statusLine(view: PlanView, input: PlanTabInput, theme?: LobbyTheme): string {
|
|
105
98
|
const parts: string[] = [];
|
|
99
|
+
const count = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
106
100
|
if (view.busy) parts.push(`${paint(theme, "accent", spinner(input.tick))} round ${view.turns}`);
|
|
107
|
-
else if (view.error) parts.push(paint(theme, "error", `✗ ${view.error.split("\n")[0]} —
|
|
108
|
-
else if (view.reply?.status === "ready") parts.push(paint(theme, "success",
|
|
109
|
-
else if (view.awaitingAnswers) {
|
|
110
|
-
|
|
111
|
-
parts.push(paint(theme, "warning", `● ${view.questions.length} question${view.questions.length === 1 ? "" : "s"} for you${left}`));
|
|
112
|
-
}
|
|
113
|
-
if (view.lineComments.length > 0) parts.push(paint(theme, "accent", `◆ ${view.lineComments.length} line comment${view.lineComments.length === 1 ? "" : "s"} to send`));
|
|
101
|
+
else if (view.error) parts.push(paint(theme, "error", `✗ ${view.error.split("\n")[0]} — r retries`));
|
|
102
|
+
else if (view.reply?.status === "ready") parts.push(paint(theme, "success", `✓ ready — ${input.saveKey ?? "Ctrl+S"} saves it`));
|
|
103
|
+
else if (view.awaitingAnswers) parts.push(paint(theme, "warning", `● ${count(view.questions.length, "question")} — enter ${view.answeredChunks > 0 ? "resumes" : "answers them"}`));
|
|
104
|
+
if (view.lineComments.length > 0) parts.push(paint(theme, "accent", `◆ ${count(view.lineComments.length, "comment")} to send`));
|
|
114
105
|
if (view.saved) parts.push(paint(theme, "success", `saved as ${view.saved.id}`));
|
|
115
106
|
if (!view.busy && view.turns > 0) parts.push(paint(theme, "dim", `round ${view.turns}`));
|
|
116
|
-
return parts.join(paint(theme, "dim", " · "))
|
|
107
|
+
return parts.join(paint(theme, "dim", " · "));
|
|
117
108
|
}
|
|
118
109
|
|
|
119
110
|
/** One seat on the roster: a spinner and its step while it works, then ready, its question count or a failure. */
|
|
@@ -124,6 +115,8 @@ function seatCell(seat: SeatView, tick: number, theme?: LobbyTheme): string {
|
|
|
124
115
|
if (seat.status === "failed") return `${name} ${paint(theme, "error", "✗ failed — r retries")}`;
|
|
125
116
|
if (seat.status === "idle") return `${name} ${paint(theme, "dim", "·")}`;
|
|
126
117
|
if (seat.ready) return `${name} ${paint(theme, "success", "✓ ready")}`;
|
|
118
|
+
// A seat whose questions the oracle settled itself has nothing waiting on you.
|
|
119
|
+
if (seat.questions === 0) return `${name} ${paint(theme, "dim", "done")}`;
|
|
127
120
|
return `${name} ${paint(theme, "warning", `${seat.questions} question${seat.questions === 1 ? "" : "s"}`)}`;
|
|
128
121
|
}
|
|
129
122
|
|
|
@@ -158,12 +151,14 @@ export function conversationLines(view: PlanView, width: number, theme?: LobbyTh
|
|
|
158
151
|
.filter(({ question }) => matches(`${question.from} ${question.text} ${question.options.map((option) => option.label).join(" ")}`, query));
|
|
159
152
|
if (message.role === "you" ? !matches(message.text, query) : message.questions?.length ? questions.length === 0 : !matches(message.text, query)) continue;
|
|
160
153
|
if (lines.length > 0 && lines.at(-1) !== "") lines.push("");
|
|
154
|
+
// The same turns as the Lobby's conversation: who speaks and when, your words in a band.
|
|
155
|
+
const time = message.at > 0 ? clock(message.at) : "";
|
|
161
156
|
if (message.role === "you") {
|
|
162
|
-
lines.push(
|
|
157
|
+
lines.push(speakerLine("you", width, theme, time), ...youLines(message.text, width, theme));
|
|
163
158
|
continue;
|
|
164
159
|
}
|
|
165
|
-
lines.push(
|
|
166
|
-
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) : wrap(message.text, width)));
|
|
160
|
+
lines.push(speakerLine("panel", width, theme, time));
|
|
161
|
+
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) : wrap(message.text, width).map((line) => (line ? ` ${line}` : ""))));
|
|
167
162
|
}
|
|
168
163
|
if (query && lines.length === 0) return wrap(paint(theme, "dim", `Nothing in the conversation matches "${query}".`), width);
|
|
169
164
|
return lines;
|
|
@@ -201,7 +196,7 @@ export function renderPlan(input: PlanTabInput, width: number, height: number, t
|
|
|
201
196
|
if (height <= 0) return [];
|
|
202
197
|
const notice = input.notice ? [paint(theme, "accent", input.notice)] : [];
|
|
203
198
|
const view = input.session;
|
|
204
|
-
if (!view) return fill([...notice, ...box(width, height - notice.length, intro(input, width - 4, theme), { title: "Plan",
|
|
199
|
+
if (!view) return fill([...notice, ...box(width, height - notice.length, intro(input, width - 4, theme), { title: "Plan", theme })], height, width);
|
|
205
200
|
const header = [
|
|
206
201
|
...notice,
|
|
207
202
|
...wrap(`${bold(theme, view.title ? `Planning · ${view.title}` : "Planning")} ${statusLine(view, input, theme)}`, width),
|
|
@@ -73,16 +73,13 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
73
73
|
return lines;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Before the first fix: one sentence, then the agent and the model it runs on. */
|
|
76
77
|
function intro(input: QuickFixTabInput, width: number, theme?: LobbyTheme): string[] {
|
|
77
78
|
return [
|
|
78
|
-
bold(theme, "
|
|
79
|
+
...wrap(bold(theme, "Describe a small change below and one agent makes it now, beside any running task."), width),
|
|
79
80
|
"",
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
"Keep them small: a rename, a typo, a one-function fix. If it turns out bigger, the agent stops and says so, and you can plan it as a task.",
|
|
83
|
-
"",
|
|
84
|
-
paint(theme, "dim", `quick fix: ${input.profile} — esc, then m changes the model, thinking, time limit and instructions (alt+s: all settings)`),
|
|
85
|
-
].flatMap((line) => wrap(line, width));
|
|
81
|
+
`${paint(theme, "mdCode", "QUICK FIX".padEnd(11))}${paint(theme, "muted", input.profile)}`,
|
|
82
|
+
];
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
/** Jobs whose prompt, steps or report mention `query`. */
|
|
@@ -104,7 +101,7 @@ export function renderQuickFix(input: QuickFixTabInput, width: number, height: n
|
|
|
104
101
|
const jobs = newestFirst(filterJobs(input.jobs, input.query));
|
|
105
102
|
if (jobs.length === 0) {
|
|
106
103
|
const content = input.query && input.jobs.length > 0 ? [paint(theme, "dim", `No quick fix mentions "${input.query}".`)] : intro(input, width - 4, theme);
|
|
107
|
-
return box(width, height, content, { title: "Quick fix", right: input.profile, theme });
|
|
104
|
+
return box(width, height, content, { title: "Quick fix", ...(input.query ? { right: input.profile } : {}), theme });
|
|
108
105
|
}
|
|
109
106
|
const selected = Math.min(Math.max(0, input.selected), jobs.length - 1);
|
|
110
107
|
const { list: listWidth, detail: detailWidth, wide } = quickFixWidths(width);
|