@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.
@@ -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, columns, fill, markdownLines, paint, rule, selectRow, since, split, windowStart, wrap, wrapHanging, type LobbyTheme } from "../layout.ts";
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
- return fill([...notice, paint(theme, "dim", "No tasks yet. Start one from the Lobby tab, or plan one in the Plan tab.")], height, width);
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 >= TASKS_COLUMNS_MIN;
201
- const [listWidth, detailWidth] = wide ? split(width, 0.38, 3, 40) : [width, width];
202
- const list = listLines(input.rows, input.selected, listWidth, input.focus === "list", theme);
203
- const listShown = list.lines.slice(windowStart(list.selectedLine, list.lines.length, bodyHeight - 1));
204
- const detailShown = input.detail.slice(Math.max(0, Math.min(input.detailOffset, input.detail.length - 1)));
205
- const listPane = fill([rule(listWidth, "Tasks", theme, `${input.rows.length}`), ...listShown], bodyHeight);
206
- const detailPane = fill([rule(detailWidth, input.focus === "detail" ? "Detail ◂" : "Detail", theme), ...detailShown], bodyHeight);
207
- if (!wide) return fill([...notice, ...(input.focus === "detail" ? detailPane : listPane)], height, width);
208
- return fill([...notice, ...columns(listPane, detailPane, listWidth, detailWidth, " │ ", theme)], height, width);
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
  }