pi-better-harness 0.32.0 → 0.32.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.
@@ -87,7 +87,7 @@ export interface AgentCommandUi {
87
87
  editor(title: string, prefill?: string): Promise<string | undefined>;
88
88
  notify(message: string, type?: "info" | "warning" | "error"): void;
89
89
  setWidget?(key: string, content: string[] | undefined): void;
90
- custom?<T>(factory: (tui: { requestRender(): void }, theme: { fg(color: string, text: string): string }, keybindings: unknown, done: (value: T) => void) => unknown, options?: { overlay: boolean }): Promise<T>;
90
+ custom?<T>(factory: (tui: { requestRender(): void }, theme: { fg(color: string, text: string): string }, keybindings: unknown, done: (value: T) => void) => unknown, options?: { overlay: boolean; overlayOptions?: import("@earendil-works/pi-tui").OverlayOptions }): Promise<T>;
91
91
  }
92
92
 
93
93
  export interface AgentCommandHost {
@@ -724,6 +724,7 @@ async function finishInspection(host: AgentCommandHost, commandResult: AgentComm
724
724
  const entries = catalog ?? { entries: [view!], diagnostics: commandResult.diagnostics, revision: "", text: "" };
725
725
  try {
726
726
  host.ui.setWidget?.("agents-catalog", undefined);
727
+ let viewportHeight = 30;
727
728
  await host.ui.custom<void>((tui, theme, _keys, done) =>
728
729
  createCatalogComponent(entries, theme, () => tui.requestRender(), () => done(), view?.id, deps.sessionSettings ? {
729
730
  get: () => presentCatalog(readCatalog(location(host, deps)), deps.enrich),
@@ -740,7 +741,13 @@ async function finishInspection(host: AgentCommandHost, commandResult: AgentComm
740
741
  }
741
742
  return { ok: saved.ok, message: saved.ok ? `Defaults saved (${saved.scope}): ${id}.` : saved.diagnostics.map((item) => item.message).join(" ") };
742
743
  },
743
- } : undefined), { overlay: true });
744
+ } : undefined, () => viewportHeight), {
745
+ overlay: true,
746
+ overlayOptions: {
747
+ anchor: "top-left", width: "100%", maxHeight: "100%", margin: 0,
748
+ visible: (_width, height) => { viewportHeight = height; return true; },
749
+ },
750
+ });
744
751
  if (deps.sessionSettings) {
745
752
  const current = readCatalog(location(host, deps));
746
753
  const refreshed = view ? presentCatalogEntry(current, view.id, deps.enrich) : presentCatalog(current, deps.enrich);
@@ -34,16 +34,18 @@ function cell(value: string, width: number): string {
34
34
 
35
35
  export function catalogColumns(entry: OperationView | null, width: number): string {
36
36
  const available = Math.max(1, width - 2);
37
- const sourceWidth = available >= 70 ? 11 : 0;
38
- const stateWidth = available >= 48 ? 10 : 0;
39
- const gaps = (sourceWidth ? 2 : 0) + (stateWidth ? 2 : 0) + 2;
40
- const modelWidth = Math.min(Math.max(1, available - gaps - 1), Math.min(28, Math.max(20, Math.floor(available * 0.3))));
41
- const nameWidth = Math.max(1, available - modelWidth - sourceWidth - stateWidth - gaps);
37
+ const sourceWidth = available >= 82 ? 11 : 0;
38
+ const stateWidth = available >= 60 ? 10 : 0;
39
+ const effortWidth = available >= 32 ? 7 : 0;
40
+ const gaps = (sourceWidth ? 2 : 0) + (stateWidth ? 2 : 0) + (effortWidth ? 2 : 0) + 2;
41
+ const modelWidth = Math.min(Math.max(1, available - gaps - effortWidth - sourceWidth - stateWidth - 1), Math.min(28, Math.max(20, Math.floor(available * 0.3))));
42
+ const nameWidth = Math.max(1, available - modelWidth - effortWidth - sourceWidth - stateWidth - gaps);
42
43
  const name = entry ? entry.identity.name || entry.id : "NAME";
43
44
  const model = entry ? modelLabel(entry) : "MODEL";
44
45
  const source = entry ? entry.identity.scope ?? "unknown" : "SOURCE";
45
46
  const status = entry ? stateLabel(entry) : "STATE";
46
- return [cell(name, nameWidth), cell(model, modelWidth), ...(sourceWidth ? [cell(source, sourceWidth)] : []), ...(stateWidth ? [cell(status, stateWidth)] : [])].join(" ").trimEnd();
47
+ const effort = entry ? entry.actualEffort ?? entry.fields?.effort.value ?? "(unset)" : "EFFORT";
48
+ return [cell(name, nameWidth), cell(model, modelWidth), ...(effortWidth ? [cell(effort, effortWidth)] : []), ...(sourceWidth ? [cell(source, sourceWidth)] : []), ...(stateWidth ? [cell(status, stateWidth)] : [])].join(" ").trimEnd();
47
49
  }
48
50
 
49
51
  function wrapLine(text: string, width: number): string[] {
@@ -60,7 +62,7 @@ function wrapLine(text: string, width: number): string[] {
60
62
  return rows;
61
63
  }
62
64
 
63
- export function catalogDetailLines(entry: OperationView): string[] {
65
+ export function catalogDetailLines(entry: OperationView, editable = false): string[] {
64
66
  const lines = [
65
67
  `${entry.identity.name || entry.id} · ${entry.identity.kind ?? "unknown"}`,
66
68
  entry.id,
@@ -73,8 +75,7 @@ export function catalogDetailLines(entry: OperationView): string[] {
73
75
  "",
74
76
  "Configuration",
75
77
  ...(entry.fields ? [
76
- fieldLine("Model", entry.fields.model),
77
- fieldLine("Effort", entry.fields.effort),
78
+ ...(!editable ? [fieldLine("Model", entry.fields.model), fieldLine("Effort", entry.fields.effort)] : []),
78
79
  fieldLine("Tier", entry.fields.tier),
79
80
  ] : []),
80
81
  `Actual ${entry.actualModel ?? "not resolved"}${entry.actualEffort ? ` · ${entry.actualEffort}` : ""}`,
@@ -95,9 +96,10 @@ export function createCatalogComponent(
95
96
  close: () => void,
96
97
  initialId?: string,
97
98
  editor?: CatalogEditor,
99
+ getHeight: () => number = () => 30,
98
100
  ) {
99
101
  let selectedId = initialId ?? catalog.entries[0]?.id;
100
- let mode: "list" | "detail" | "prompt" | "settings" = initialId ? "detail" : "list";
102
+ let mode: "list" | "detail" | "prompt" = initialId ? "detail" : "list";
101
103
  let query = "";
102
104
  let scroll = 0;
103
105
  let settingRow = 0;
@@ -159,7 +161,7 @@ export function createCatalogComponent(
159
161
  const selected = () => filtered().find((entry) => entry.id === selectedId);
160
162
  const detailRows = (entry: OperationView, width: number) => (mode === "prompt"
161
163
  ? [`${entry.identity.name || entry.id} · effective instructions`, "", ...(entry.instructions?.split("\n") ?? ["(none)"])]
162
- : catalogDetailLines(entry))
164
+ : catalogDetailLines(entry, !!editor))
163
165
  .flatMap((text) => text ? wrapLine(text, Math.max(1, width - 2)) : [""]);
164
166
  let lastDetailWidth = 80;
165
167
 
@@ -168,21 +170,32 @@ export function createCatalogComponent(
168
170
  set focused(value: boolean) { focused = value; search.focused = value && !!picker && pickerKey === "model"; },
169
171
  render(width: number): string[] {
170
172
  const safeWidth = Math.max(1, width);
173
+ const height = Math.max(1, Math.floor(getHeight()));
174
+ const footerRows = (editor && mode !== "prompt" ? 2 : 0) + (message ? 2 : 0);
175
+ const diagnosticRows = Math.min(catalog.diagnostics.length, 3);
176
+ const listSize = Math.max(1, height - 5 - footerRows - diagnosticRows - 1);
177
+ const controlRows = editor && mode === "detail" ? (safeWidth < 42 ? 5 : 3) : 0;
178
+ const detailSize = Math.max(1, height - 5 - footerRows - controlRows);
179
+ const fit = (lines: string[]) => {
180
+ const result = lines.slice(0, height).map(line);
181
+ while (result.length < height) result.push("");
182
+ return result;
183
+ };
171
184
  lastDetailWidth = safeWidth;
172
185
  const line = (text: string) => truncateToWidth(text, safeWidth);
173
186
  const rows = filtered();
174
187
  const current = selected();
175
188
  const detailLines = current ? detailRows(current, safeWidth) : [];
176
189
  const selectedIndex = rows.findIndex((entry) => entry.id === selectedId);
177
- const start = Math.max(0, Math.min(selectedIndex - 6, rows.length - 14));
178
- if (picker) return [
190
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(listSize / 2), rows.length - listSize));
191
+ scroll = Math.max(0, Math.min(scroll, detailLines.length - detailSize));
192
+ if (picker) return fit([
179
193
  theme.fg("accent", `Agents / ${pickerKey}`), "",
180
194
  ...(pickerKey === "model" ? search.render(safeWidth) : []),
181
195
  ...picker.render(safeWidth),
182
196
  ...(message ? [theme.fg(error ? "error" : "muted", message)] : []),
183
- ].map(line);
184
- const lines = mode === "settings" && current ? [
185
- theme.fg("accent", `Agents / ${current.identity.name || current.id}`), "",
197
+ ]);
198
+ const controls = editor && mode === "detail" && current ? [
186
199
  ...["Model", "Effort"].flatMap((label, index) => {
187
200
  const key = index === 0 ? "model" : "effort";
188
201
  const field = current.fields?.[key];
@@ -194,38 +207,41 @@ export function createCatalogComponent(
194
207
  const text = `${settingRow === index ? "> " : " "}${label.padEnd(18)} ${value}`;
195
208
  if (safeWidth < 42) {
196
209
  const title = `${settingRow === index ? "> " : " "}${label}`;
197
- return [settingRow === index ? highlight(title, safeWidth) : title, ...wrapLine(value, Math.max(1, safeWidth - 2)).map((line) => ` ${line}`)];
210
+ return [settingRow === index ? highlight(title, safeWidth) : title, ` ${truncateToWidth(value, Math.max(1, safeWidth - 2))}`];
198
211
  }
199
212
  return [settingRow === index ? highlight(text, safeWidth) : text];
200
213
  }),
201
- ] : mode !== "list" && current ? [
202
- theme.fg("accent", mode === "prompt" ? "Agents / prompt" : "Agents / inspect"),
203
- theme.fg("dim", mode === "prompt" ? "← details · ↑↓ scroll · Esc close" : `${editor ? "e settings · " : ""}p prompt · ← list · ↑↓ scroll · Esc close`),
204
214
  "",
205
- ...detailLines.slice(scroll, scroll + 22).map((text, i) =>
215
+ ] : [];
216
+ const lines = mode !== "list" && current ? [
217
+ theme.fg("accent", mode === "prompt" ? "Agents / prompt" : editor ? `Agents / ${current.identity.name || current.id}` : "Agents / inspect"),
218
+ theme.fg("dim", mode === "prompt" ? "← details · ↑↓ scroll · Esc back" : `${editor ? "Enter change · " : ""}p prompt · ← list · ↑↓ ${editor ? "select / scroll" : "scroll"} · Esc back`),
219
+ "",
220
+ ...controls,
221
+ ...detailLines.slice(scroll, scroll + detailSize).map((text, i) =>
206
222
  i === 0 && scroll === 0 ? theme.fg("accent", text) : text),
207
223
  "",
208
- theme.fg("dim", `Showing ${scroll + 1}-${Math.min(scroll + 22, detailLines.length)} of ${detailLines.length} lines`),
224
+ theme.fg("dim", `Showing ${scroll + 1}-${Math.min(scroll + detailSize, detailLines.length)} of ${detailLines.length} lines`),
209
225
  ] : [
210
226
  theme.fg("accent", `Agents · ${catalog.entries.length} definitions`),
211
227
  theme.fg("dim", "Type to filter · ↑↓ select · Enter inspect · Esc close"),
212
228
  `Search ${query || "(all roles and agents)"}`,
213
229
  "",
214
230
  theme.fg("dim", ` ${catalogColumns(null, safeWidth)}`),
215
- ...rows.slice(start, start + 14).map((entry) => {
231
+ ...rows.slice(start, start + listSize).map((entry) => {
216
232
  const active = selectedId === entry.id;
217
233
  const marker = active ? "›" : " ";
218
234
  const text = `${marker} ${catalogColumns(entry, safeWidth)}`;
219
235
  return active ? highlight(text, safeWidth) : text;
220
236
  }),
221
237
  ...(rows.length === 0 ? [" No matching definitions"] : []),
222
- ...(rows.length > 14 ? [theme.fg("dim", ` ${start + 1}-${Math.min(start + 14, rows.length)} of ${rows.length}`)] : []),
238
+ ...(rows.length > listSize ? [theme.fg("dim", ` ${start + 1}-${Math.min(start + listSize, rows.length)} of ${rows.length}`)] : []),
223
239
  ...catalog.diagnostics.slice(0, 2).map((diagnostic) => theme.fg("warning", `! ${diagnostic.message}`)),
224
240
  ...(catalog.diagnostics.length > 2 ? [theme.fg("warning", `! ${catalog.diagnostics.length - 2} more catalog diagnostics`)] : []),
225
241
  ];
226
- if (editor && mode !== "prompt") lines.push("", theme.fg("dim", mode === "settings" ? "Up/Down Select · Enter Change · ctrl+s Save defaults · Esc Back" : "ctrl+s Save selected defaults"));
242
+ if (editor && mode !== "prompt") lines.push("", theme.fg("dim", "ctrl+s Save selected defaults"));
227
243
  if (message) lines.push("", theme.fg(error ? "error" : "muted", message));
228
- return lines.map(line);
244
+ return fit(lines);
229
245
  },
230
246
  handleInput(data: string): void {
231
247
  const rows = filtered();
@@ -239,25 +255,26 @@ export function createCatalogComponent(
239
255
  }
240
256
  if (editor && matchesKey(data, "ctrl+s")) { save(); return; }
241
257
  if (matchesKey(data, "escape") || matchesKey(data, "left")) {
242
- if (mode === "prompt" || mode === "settings") { mode = "detail"; scroll = 0; }
258
+ if (mode === "prompt") { mode = "detail"; scroll = 0; }
243
259
  else if (mode === "detail") { mode = "list"; scroll = 0; }
244
260
  else if (query) { query = ""; selectedId = catalog.entries[0]?.id; }
245
261
  else { close(); return; }
246
262
  } else if (matchesKey(data, "up") || matchesKey(data, "down")) {
247
263
  const delta = matchesKey(data, "down") ? 1 : -1;
248
- if (mode === "settings") settingRow = Math.max(0, Math.min(1, settingRow + delta));
264
+ if (mode === "detail" && editor && scroll === 0 && (delta < 0 || settingRow === 0)) settingRow = Math.max(0, Math.min(1, settingRow + delta));
249
265
  else if (mode !== "list" && selected()) scroll = Math.max(0, Math.min(detailRows(selected()!, lastDetailWidth).length - 1, scroll + delta));
250
266
  else if (rows.length) selectedId = rows[Math.max(0, Math.min(rows.length - 1, rows.findIndex((entry) => entry.id === selectedId) + delta))]?.id;
251
267
  } else if (matchesKey(data, "enter") && mode === "list" && selected()) {
252
268
  mode = "detail";
269
+ settingRow = 0;
253
270
  scroll = 0;
254
271
  } else if ((data === "p" || data === "P") && mode === "detail" && selected()) {
255
272
  mode = "prompt";
256
273
  scroll = 0;
257
274
  } else if (editor && (data === "e" || data === "E") && mode === "detail" && selected()) {
258
- mode = "settings";
259
275
  settingRow = 0;
260
- } else if (mode === "settings" && (matchesKey(data, "enter") || data === " ")) {
276
+ scroll = 0;
277
+ } else if (editor && mode === "detail" && (matchesKey(data, "enter") || data === " ")) {
261
278
  try { edit(); } catch (cause) { report(cause instanceof Error ? cause.message : String(cause), true); }
262
279
  } else if (mode === "list" && matchesKey(data, "backspace")) {
263
280
  query = query.slice(0, -1);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -58,7 +58,7 @@
58
58
  "pi-better-plan": "0.9.1",
59
59
  "pi-better-sandbox": "0.13.0",
60
60
  "pi-better-ssh": "0.1.3",
61
- "pi-better-subagents": "0.20.0",
61
+ "pi-better-subagents": "0.20.1",
62
62
  "proper-lockfile": "^4.1.2",
63
63
  "smol-toml": "1.9.0",
64
64
  "yaml": "^2.9.1"