@a-t-h-i/bot-lobby 0.5.0 → 0.5.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 +146 -51
- package/package.json +5 -2
- package/prompts/panel.md +8 -3
- package/prompts/planner.md +9 -4
- package/src/lobby/ask.ts +226 -0
- package/src/lobby/keys.ts +51 -0
- package/src/lobby/layout.ts +192 -3
- package/src/lobby/markdown.ts +48 -0
- package/src/lobby/planner.ts +121 -14
- package/src/lobby/runtime.ts +158 -7
- package/src/lobby/tabs/home.ts +121 -43
- package/src/lobby/tabs/metrics.ts +155 -42
- package/src/lobby/tabs/plan.ts +117 -35
- package/src/lobby/tabs/quickfix.ts +40 -12
- package/src/lobby/tabs/tasks.ts +46 -12
- package/src/lobby/view.ts +868 -113
- package/src/pi/commands.ts +1 -1
- package/src/pi/settings-ui.ts +65 -1
- package/src/schemas/configuration.ts +38 -4
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -11,7 +11,7 @@ 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 { ago, bold,
|
|
14
|
+
import { ago, beside, bold, box, detailWindow, fill, markdownLines, notePane, paint, position, rule, selectRow, since, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
15
15
|
|
|
16
16
|
export type TaskSection = "mine" | "others" | "pending" | "recent";
|
|
17
17
|
|
|
@@ -180,30 +180,64 @@ export function planDetailLines(plan: PlannedTask, width: number, now: number, t
|
|
|
180
180
|
export interface TasksInput {
|
|
181
181
|
rows: readonly TaskRow[];
|
|
182
182
|
selected: number;
|
|
183
|
-
/** Detail lines for the selected row. */
|
|
183
|
+
/** Detail lines for the selected row, already fitted to the detail pane. */
|
|
184
184
|
detail: readonly string[];
|
|
185
185
|
focus: "list" | "detail";
|
|
186
186
|
detailOffset: number;
|
|
187
187
|
notice?: string;
|
|
188
|
+
/** The search in force; the rows are already filtered by it. */
|
|
189
|
+
query?: string;
|
|
190
|
+
/** Filled with where the list and the detail landed, for scrolling. */
|
|
191
|
+
panes?: PaneLayout;
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
/** Narrow terminals show the list or the detail, not both. */
|
|
191
195
|
export const TASKS_COLUMNS_MIN = 90;
|
|
192
196
|
|
|
197
|
+
/** Outer widths of the list and detail panes (the detail's text is 4 columns narrower). */
|
|
198
|
+
export function tasksWidths(width: number): { list: number; detail: number; wide: boolean } {
|
|
199
|
+
if (width < TASKS_COLUMNS_MIN) return { list: width, detail: width, wide: false };
|
|
200
|
+
const list = Math.max(36, Math.round((width - 1) * 0.36));
|
|
201
|
+
return { list, detail: width - 1 - list, wide: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Everything a search looks at for one row: id, title, request, proposal, plan. */
|
|
205
|
+
function rowText(row: TaskRow, tasks: readonly Task[], plans: readonly PlannedTask[]): string {
|
|
206
|
+
if (row.kind === "plan") {
|
|
207
|
+
const plan = plans.find((entry) => entry.id === row.id);
|
|
208
|
+
return [row.id, row.title, plan?.brief ?? "", plan?.issue?.title ?? ""].join("\n");
|
|
209
|
+
}
|
|
210
|
+
const task = tasks.find((entry) => entry.id === row.id);
|
|
211
|
+
return [row.id, row.title, row.status, task ? taskRequest(task) : "", task?.proposal ?? "", task?.plan ?? ""].join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The rows whose task or plan mentions `query` anywhere. */
|
|
215
|
+
export function filterRows(rows: readonly TaskRow[], tasks: readonly Task[], plans: readonly PlannedTask[], query: string | undefined): TaskRow[] {
|
|
216
|
+
const needle = query?.trim().toLowerCase();
|
|
217
|
+
if (!needle) return [...rows];
|
|
218
|
+
return rows.filter((row) => rowText(row, tasks, plans).toLowerCase().includes(needle));
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
export function renderTasks(input: TasksInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
194
222
|
if (height <= 0) return [];
|
|
195
223
|
const notice = input.notice ? [paint(theme, "accent", input.notice)] : [];
|
|
196
224
|
const bodyHeight = height - notice.length;
|
|
197
225
|
if (input.rows.length === 0) {
|
|
198
|
-
|
|
226
|
+
const empty = input.query ? `No task or plan mentions "${input.query}".` : "No tasks yet. Start one from the Lobby tab, or plan one in the Plan tab.";
|
|
227
|
+
return fill([...notice, ...box(width, bodyHeight, [paint(theme, "dim", empty)], { title: "Tasks", theme })], height, width);
|
|
199
228
|
}
|
|
200
|
-
const wide = width
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const listPane =
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
const { list: listWidth, detail: detailWidth, wide } = tasksWidths(width);
|
|
230
|
+
const list = listLines(input.rows, input.selected, listWidth - 4, input.focus === "list", theme);
|
|
231
|
+
const rows = Math.max(0, bodyHeight - 2);
|
|
232
|
+
const listStart = windowStart(list.selectedLine, list.lines.length, rows);
|
|
233
|
+
const count = input.query ? `${input.rows.length} match${input.rows.length === 1 ? "" : "es"}` : `${input.rows.length}`;
|
|
234
|
+
const listPane = box(listWidth, bodyHeight, list.lines.slice(listStart), { title: "Tasks", right: count, focused: input.focus === "list", scroll: { total: list.lines.length, start: listStart }, theme });
|
|
235
|
+
const detailStart = detailWindow(input.detail.length, rows, input.detailOffset);
|
|
236
|
+
const detailPane = box(detailWidth, bodyHeight, input.detail.slice(detailStart), { title: "Detail", ...(input.detail.length > rows ? { right: position(detailStart, rows, input.detail.length) } : {}), focused: input.focus === "detail", scroll: { total: input.detail.length, start: detailStart }, theme });
|
|
237
|
+
const showList = wide || input.focus !== "detail";
|
|
238
|
+
const showDetail = wide || input.focus === "detail";
|
|
239
|
+
if (showList) notePane(input.panes, "list", notice.length, 0, listWidth, bodyHeight, list.lines.length);
|
|
240
|
+
if (showDetail) notePane(input.panes, "detail", notice.length, wide ? listWidth + 1 : 0, detailWidth, bodyHeight, input.detail.length);
|
|
241
|
+
if (!wide) return fill([...notice, ...(showDetail ? detailPane : listPane)], height, width);
|
|
242
|
+
return fill([...notice, ...beside([listPane, detailPane])], height, width);
|
|
209
243
|
}
|