@jwilger/pi-development-system 0.48.0 → 0.51.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.
@@ -1,23 +1,19 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ // biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
3
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
4
  import type { ConfigStore } from "../prefs/config.ts";
4
- import {
5
- canOpenDialog,
6
- dialogInput,
7
- dialogMenu,
8
- withDialogSession,
9
- } from "./dialog.ts";
10
5
  import {
11
6
  DEFAULT_MANAGER_SETTINGS,
12
7
  loadManagerSaveScope,
13
- saveManagerSaveScope,
14
- saveManagerSettings,
15
8
  type ManagerSettings,
16
9
  type ModelSelectionMode,
17
10
  type SubagentMode,
11
+ saveManagerSaveScope,
12
+ saveManagerSettings,
18
13
  type ToolFilteringMode,
19
14
  type WidgetMode,
20
15
  } from "../prefs/settings.ts";
16
+ import { canOpenDialog, dialogInput, dialogMenu, withDialogSession } from "./dialog.ts";
21
17
  import { editAgentTypes } from "./ui.ts";
22
18
 
23
19
  const FIELDS = [
@@ -138,9 +134,7 @@ export async function configureAgents(
138
134
  },
139
135
  ): Promise<void> {
140
136
  if (!canOpenDialog(ctx)) return;
141
- await withDialogSession(ctx, (scoped) =>
142
- configureAgentsDialog(scoped, options),
143
- );
137
+ await withDialogSession(ctx, (scoped) => configureAgentsDialog(scoped, options));
144
138
  }
145
139
 
146
140
  async function configureAgentsDialog(
@@ -167,13 +161,13 @@ async function configureAgentsDialog(
167
161
  {
168
162
  id: "subagentMode",
169
163
  label: "Subagent Mode",
170
- value: MODE_OPTIONS.find((mode) => mode.id === draft.subagentMode)!.label,
164
+ value: MODE_OPTIONS.find((mode) => mode.id === draft.subagentMode)?.label,
171
165
  help: MODE_HELP,
172
166
  },
173
167
  {
174
168
  id: "toolFiltering",
175
169
  label: "Tool Filtering",
176
- value: TOOL_FILTERING_OPTIONS.find((mode) => mode.id === draft.toolFiltering)!.label,
170
+ value: TOOL_FILTERING_OPTIONS.find((mode) => mode.id === draft.toolFiltering)?.label,
177
171
  help: TOOL_FILTERING_HELP,
178
172
  },
179
173
  ...FIELDS.map((field) => ({
@@ -183,13 +177,13 @@ async function configureAgentsDialog(
183
177
  {
184
178
  id: "modelSelection",
185
179
  label: "Model Picking",
186
- value: MODEL_SELECTION_OPTIONS.find((mode) => mode.id === draft.modelSelection)!.label,
180
+ value: MODEL_SELECTION_OPTIONS.find((mode) => mode.id === draft.modelSelection)?.label,
187
181
  help: MODEL_SELECTION_HELP,
188
182
  },
189
183
  {
190
184
  id: "widgetMode",
191
185
  label: "Status Widget",
192
- value: WIDGET_MODE_OPTIONS.find((mode) => mode.id === draft.widgetMode)!.label,
186
+ value: WIDGET_MODE_OPTIONS.find((mode) => mode.id === draft.widgetMode)?.label,
193
187
  help: WIDGET_MODE_HELP,
194
188
  },
195
189
  {
@@ -225,7 +219,7 @@ async function configureAgentsDialog(
225
219
  id: "save",
226
220
  label: "Save and apply",
227
221
  value: dirty ? "(changes)" : undefined,
228
- valueColor: dirty ? "warning" as const : undefined,
222
+ valueColor: dirty ? ("warning" as const) : undefined,
229
223
  help: "Save all values to the selected scope and reload settings now.\nProject overrides still take precedence.\nRunning agents and retained sessions are kept.",
230
224
  },
231
225
  {
@@ -243,19 +237,13 @@ async function configureAgentsDialog(
243
237
  if (!action || action === "cancel") return;
244
238
  selectedId = action;
245
239
  try {
246
- const field = FIELDS.find((field) => field.id === action);
240
+ const field = FIELDS.find((candidate) => candidate.id === action);
247
241
  if (field) {
248
- const value = await dialogInput(
249
- ctx,
250
- field.label,
251
- String(draft[field.id]),
252
- field.help,
253
- );
242
+ const value = await dialogInput(ctx, field.label, String(draft[field.id]), field.help);
254
243
  if (value === undefined) continue;
255
244
  const number = Number(value.trim());
256
245
  if (
257
- !/^\d+$/.test(value.trim()) ||
258
- !Number.isSafeInteger(number) ||
246
+ !(/^\d+$/.test(value.trim()) && Number.isSafeInteger(number)) ||
259
247
  number < 1 ||
260
248
  (field.id === "maxLevels" && number > 32)
261
249
  )
@@ -291,10 +279,7 @@ async function configureAgentsDialog(
291
279
  draft.finalRecap = !draft.finalRecap;
292
280
  } else if (action === "scope") {
293
281
  if (!ctx.isProjectTrusted())
294
- ctx.ui.notify(
295
- "Project settings require a trusted project; saving globally.",
296
- "warning",
297
- );
282
+ ctx.ui.notify("Project settings require a trusted project; saving globally.", "warning");
298
283
  else {
299
284
  const nextScope = scope === "user" ? "project" : "user";
300
285
  saveManagerSaveScope(options.agentDir, nextScope);
@@ -320,10 +305,7 @@ async function configureAgentsDialog(
320
305
  return;
321
306
  }
322
307
  } catch (error) {
323
- ctx.ui.notify(
324
- error instanceof Error ? error.message : String(error),
325
- "error",
326
- );
308
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
327
309
  }
328
310
  }
329
311
  }
@@ -1,4 +1,7 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ // biome-ignore-all lint/complexity/noVoid: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
3
+ // biome-ignore-all lint/suspicious/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
4
+ // biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
5
  import type {
3
6
  ExtensionCommandContext,
4
7
  ExtensionContext,
@@ -7,30 +10,34 @@ import type {
7
10
  } from "@earendil-works/pi-coding-agent";
8
11
  import {
9
12
  getKeybindings,
10
- setKeybindings,
11
13
  Key,
12
- matchesKey,
13
14
  type Keybinding,
15
+ matchesKey,
16
+ setKeybindings,
14
17
  } from "@earendil-works/pi-tui";
18
+ import type { ThreadService } from "../types.ts";
15
19
  import {
16
20
  canOpenDialog,
17
21
  DIALOG_OPTIONS,
18
22
  type DialogHost,
19
23
  DialogSession,
20
- scopeDialogContext,
21
- dialogMenu,
22
24
  dialogEditor,
23
25
  dialogHeight,
26
+ dialogMenu,
24
27
  dialogText,
25
28
  frameDialog,
29
+ scopeDialogContext,
26
30
  } from "./dialog.ts";
27
- import type { ThreadService } from "../types.ts";
31
+ import { type AgentViewportState, fillViewport, LiveAgentView } from "./live-agent-view.ts";
28
32
  import { buildStatusTree, type StatusRow } from "./thread-tree.ts";
29
33
  import {
30
- AGENT_PROGRESS_INTERVAL, agentProgressIcon, agentTypeBadge, agentTypeLabel, showThreads, threadMetrics,
34
+ AGENT_PROGRESS_INTERVAL,
35
+ agentProgressIcon,
36
+ agentTypeBadge,
37
+ agentTypeLabel,
38
+ showThreads,
39
+ threadMetrics,
31
40
  } from "./ui.ts";
32
- import { LiveAgentView, fillViewport, type AgentViewportState } from "./live-agent-view.ts";
33
-
34
41
 
35
42
  const ROOT = "/root";
36
43
  const INACTIVE_STATES = new Set(["paused", "stopped", "failed", "completed"]);
@@ -55,7 +62,7 @@ function lexicalParent(path: string): string | null {
55
62
  function treeParent(rows: StatusRow[], index: number): StatusRow | undefined {
56
63
  const depth = rows[index]?.prefix.length ?? 0;
57
64
  for (let cursor = index - 1; cursor >= 0; cursor--) {
58
- if (rows[cursor]!.prefix.length < depth) return rows[cursor];
65
+ if ((rows[cursor]?.prefix.length ?? 0) < depth) return rows[cursor];
59
66
  }
60
67
  return undefined;
61
68
  }
@@ -96,24 +103,59 @@ class StatusDialog {
96
103
  private refreshDelay = 0;
97
104
  private refreshing = false;
98
105
 
106
+ private host: DialogHost;
107
+
108
+ private theme: Theme;
109
+
110
+ private service: ThreadService;
111
+
112
+ private done: (path: string | undefined) => void;
113
+
114
+ private title: string;
115
+
116
+ private navigation:
117
+ | {
118
+ state: AgentTreeState;
119
+ actions(path: string): void;
120
+ fullscreen?: boolean;
121
+ }
122
+ | undefined;
123
+
124
+ private nerdFontIcons: boolean;
125
+
99
126
  constructor(
100
- private host: DialogHost,
101
- private theme: Theme,
102
- private service: ThreadService,
103
- private done: (path: string | undefined) => void,
127
+ host: DialogHost,
128
+ theme: Theme,
129
+ service: ThreadService,
130
+ done: (path: string | undefined) => void,
104
131
  selected?: string,
105
- private title = TITLE,
106
- private navigation?: {
132
+ title: string = TITLE,
133
+ navigation?: {
107
134
  state: AgentTreeState;
108
135
  actions(path: string): void;
109
136
  fullscreen?: boolean;
110
137
  },
111
- private nerdFontIcons = false,
138
+ nerdFontIcons: boolean = false,
112
139
  ) {
140
+ this.host = host;
141
+
142
+ this.theme = theme;
143
+
144
+ this.service = service;
145
+
146
+ this.done = done;
147
+
148
+ this.title = title;
149
+
150
+ this.navigation = navigation;
151
+
152
+ this.nerdFontIcons = nerdFontIcons;
113
153
  this.state = navigation?.state ?? { selectedPath: selected || ROOT, collapsed: new Set() };
114
154
  }
115
155
 
116
- invalidate(): void {}
156
+ invalidate(): void {
157
+ // Nothing is cached, so there is nothing to invalidate.
158
+ }
117
159
 
118
160
  /** Poll retained status, accelerating the existing timer only for animated live agents. */
119
161
  startRefresh(): void {
@@ -130,9 +172,15 @@ class StatusDialog {
130
172
 
131
173
  private syncRefresh(): void {
132
174
  if (!this.refreshing) return;
133
- const delay = this.nerdFontIcons && this.service.list().some((thread) =>
134
- thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"))
135
- ? AGENT_PROGRESS_INTERVAL : 1000;
175
+ const animated =
176
+ this.nerdFontIcons &&
177
+ this.service
178
+ .list()
179
+ .some(
180
+ (thread) =>
181
+ thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"),
182
+ );
183
+ const delay = animated ? AGENT_PROGRESS_INTERVAL : 1000;
136
184
  if (this.timer !== undefined && this.refreshDelay === delay) return;
137
185
  this.stopRefresh();
138
186
  this.refreshDelay = delay;
@@ -154,14 +202,18 @@ class StatusDialog {
154
202
  private stopAll(): void {
155
203
  const live = this.service
156
204
  .list()
157
- .filter((thread) => thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"))
205
+ .filter(
206
+ (thread) =>
207
+ thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"),
208
+ )
158
209
  .map((thread) => thread.path);
159
210
  const tops = live.filter((path) => !live.some((other) => path.startsWith(`${other}/`)));
160
211
  for (const path of tops) {
161
- void this.service
212
+ this.service
162
213
  .stop(path)
163
- .catch(() => {})
164
- .finally(() => this.host.requestRender());
214
+ .catch(() => undefined)
215
+ .finally(() => this.host.requestRender())
216
+ .catch(() => undefined);
165
217
  }
166
218
  this.host.requestRender();
167
219
  }
@@ -241,8 +293,18 @@ class StatusDialog {
241
293
  this.host.requestRender();
242
294
  }
243
295
 
296
+ private markerFor(row: StatusRow): string {
297
+ if (!row.hasChildren) return "";
298
+ return this.collapsed.has(row.path) ? "▸ " : "▾ ";
299
+ }
300
+
301
+ private missingThreadText(row: StatusRow | undefined): string {
302
+ if (row?.path !== ROOT) return "Missing parent";
303
+ return agentCount(this.service.list()) ? "Main Pi session" : "Main Pi session — No agents yet";
304
+ }
305
+
244
306
  private line(row: StatusRow, selected: boolean): string {
245
- const marker = row.hasChildren ? (this.collapsed.has(row.path) ? "▸ " : "▾ ") : "";
307
+ const marker = this.markerFor(row);
246
308
  const head = ` ${row.prefix}${selected ? "›" : " "} ${marker}`;
247
309
  const thread = row.thread;
248
310
  if (!thread) {
@@ -253,14 +315,16 @@ class StatusDialog {
253
315
  if (inactive) {
254
316
  // Completed fades furthest (dim); the state word keeps a theme hint for done/failed.
255
317
  const base = thread.state === "completed" ? "dim" : "muted";
256
- const hint =
257
- thread.state === "completed" ? "success" : thread.state === "failed" ? "error" : base;
318
+ const hint = hintFor(thread.state, base);
258
319
  const state = dialogText(thread.state);
259
320
  const path = dialogText(thread.path);
260
321
  const status = dialogText(thread.status);
261
322
  return [
262
323
  this.theme.fg(selected ? "accent" : base, head),
263
- this.theme.fg(base, `${agentTypeLabel(thread.type, thread.icon, this.nerdFontIcons)} ${path} `),
324
+ this.theme.fg(
325
+ base,
326
+ `${agentTypeLabel(thread.type, thread.icon, this.nerdFontIcons)} ${path} `,
327
+ ),
264
328
  this.theme.fg(hint, state),
265
329
  this.theme.fg(base, ` ${status}`),
266
330
  ].join("");
@@ -268,8 +332,13 @@ class StatusDialog {
268
332
  const rest = [thread.path, thread.state, thread.status]
269
333
  .map((part) => dialogText(part))
270
334
  .join(" ");
271
- const badge = agentTypeBadge(thread.type, thread.color, this.theme,
272
- agentProgressIcon(thread, this.nerdFontIcons), this.nerdFontIcons);
335
+ const badge = agentTypeBadge(
336
+ thread.type,
337
+ thread.color,
338
+ this.theme,
339
+ agentProgressIcon(thread, this.nerdFontIcons),
340
+ this.nerdFontIcons,
341
+ );
273
342
  if (selected)
274
343
  return `${this.theme.fg("accent", head)}${badge} ${this.theme.fg("accent", rest)}`;
275
344
  return `${head}${badge} ${rest}`;
@@ -277,10 +346,7 @@ class StatusDialog {
277
346
 
278
347
  private detail(row: StatusRow | undefined): string[] {
279
348
  if (!row?.thread) {
280
- return [
281
- ` ${row?.path === ROOT ? (agentCount(this.service.list()) ? "Main Pi session" : "Main Pi session — No agents yet") : "Missing parent"}`,
282
- "",
283
- ];
349
+ return [` ${this.missingThreadText(row)}`, ""];
284
350
  }
285
351
  return [` task ${dialogText(row.thread.task)}`, ` ${threadMetrics(row.thread)}`];
286
352
  }
@@ -312,9 +378,7 @@ class StatusDialog {
312
378
  height,
313
379
  this.heading(rows, start, treeCount),
314
380
  body,
315
- this.navigation
316
- ? "Esc main · ↑↓ select · ←→ fold · i Actions · Ctrl+C stop all"
317
- : FOOTER,
381
+ this.navigation ? "Esc main · ↑↓ select · ←→ fold · i Actions · Ctrl+C stop all" : FOOTER,
318
382
  );
319
383
  }
320
384
 
@@ -333,10 +397,10 @@ async function showAgentDialog(
333
397
  ctx: ExtensionCommandContext,
334
398
  service: ThreadService,
335
399
  options: {
336
- title?: string;
337
- selected?: string;
338
- inspect?: (path: string) => Promise<void>;
339
- nerdFontIcons?: boolean;
400
+ title?: string | undefined;
401
+ selected?: string | undefined;
402
+ inspect?: ((path: string) => Promise<void>) | undefined;
403
+ nerdFontIcons?: boolean | undefined;
340
404
  } = {},
341
405
  ): Promise<void> {
342
406
  const open = options.inspect ?? ((path: string) => showThreads(ctx, service, path));
@@ -347,7 +411,16 @@ async function showAgentDialog(
347
411
  let dialog: StatusDialog | undefined;
348
412
  try {
349
413
  const chosen = await ctx.ui.custom<string | undefined>((host, theme, _keys, done) => {
350
- dialog = new StatusDialog(host, theme, service, done, selected, options.title, undefined, options.nerdFontIcons);
414
+ dialog = new StatusDialog(
415
+ host,
416
+ theme,
417
+ service,
418
+ done,
419
+ selected,
420
+ options.title,
421
+ undefined,
422
+ options.nerdFontIcons,
423
+ );
351
424
  dialog.startRefresh();
352
425
  return dialog;
353
426
  }, DIALOG_OPTIONS);
@@ -390,7 +463,12 @@ export class AgentNavigationController {
390
463
  this.session = undefined;
391
464
  }
392
465
 
393
- open(ctx: ExtensionContext, service: ThreadService, selectedPath?: string, nerdFontIcons = false): Promise<void> {
466
+ open(
467
+ ctx: ExtensionContext,
468
+ service: ThreadService,
469
+ selectedPath?: string,
470
+ nerdFontIcons = false,
471
+ ): Promise<void> {
394
472
  if (this.opening) return this.opening;
395
473
  if (!canOpenDialog(ctx)) return Promise.resolve();
396
474
  if (selectedPath) this.tree.selectedPath = selectedPath;
@@ -481,14 +559,20 @@ export class AgentNavigationController {
481
559
  await showThreads(scoped, service, chosen.path);
482
560
  continue;
483
561
  }
484
- let viewport = this.viewports.get(chosen.path);
485
- if (!viewport) {
486
- viewport = { scrollTop: 0, follow: true };
487
- this.viewports.set(chosen.path, viewport);
488
- }
562
+ const existing = this.viewports.get(chosen.path);
563
+ const viewport: AgentViewportState = existing ?? { scrollTop: 0, follow: true };
564
+ if (existing === undefined) this.viewports.set(chosen.path, viewport);
489
565
  const result = await session.mount<"back" | "main">(
490
566
  (tui, activeTheme, _keys, finish) =>
491
- new LiveAgentView(tui, activeTheme, service, chosen.path, viewport!, finish, nerdFontIcons),
567
+ new LiveAgentView(
568
+ tui,
569
+ activeTheme,
570
+ service,
571
+ chosen.path,
572
+ viewport,
573
+ finish,
574
+ nerdFontIcons,
575
+ ),
492
576
  );
493
577
  if (result !== "back") break;
494
578
  }
@@ -529,7 +613,7 @@ export class AgentNavigationController {
529
613
 
530
614
  const navigationByService = new WeakMap<ThreadService, AgentNavigationController>();
531
615
 
532
- async function showAgentTree(
616
+ async function _showAgentTree(
533
617
  ctx: ExtensionContext,
534
618
  service: ThreadService,
535
619
  selectedPath?: string,
@@ -542,3 +626,8 @@ async function showAgentTree(
542
626
  }
543
627
  await navigation.open(ctx, service, selectedPath, nerdFontIcons);
544
628
  }
629
+
630
+ function hintFor<T extends string>(state: string, base: T): T | "success" | "error" {
631
+ if (state === "completed") return "success";
632
+ return state === "failed" ? "error" : base;
633
+ }
@@ -25,11 +25,7 @@ function comparePath(a: string, b: string): number {
25
25
  return a < b ? -1 : a > b ? 1 : 0;
26
26
  }
27
27
 
28
- function chainHas(
29
- parentOf: Map<string, string | null>,
30
- start: string,
31
- target: string,
32
- ): boolean {
28
+ function chainHas(parentOf: Map<string, string | null>, start: string, target: string): boolean {
33
29
  const seen = new Set<string>();
34
30
  let current: string | null = start;
35
31
  while (current) {
@@ -50,8 +46,7 @@ export function buildStatusTree(
50
46
  ): StatusRow[] {
51
47
  const threadsByPath = new Map<string, ThreadView>();
52
48
  for (const thread of threads) {
53
- if (thread?.path && !threadsByPath.has(thread.path))
54
- threadsByPath.set(thread.path, thread);
49
+ if (thread?.path && !threadsByPath.has(thread.path)) threadsByPath.set(thread.path, thread);
55
50
  }
56
51
  const nodes = new Map<string, StatusNode>();
57
52
  const parentOf = new Map<string, string | null>();
@@ -66,8 +61,7 @@ export function buildStatusTree(
66
61
  const declaredParent = (path: string): string | null => {
67
62
  if (path === ROOT) return null;
68
63
  const thread = threadsByPath.get(path);
69
- if (thread)
70
- return thread.parent && thread.parent !== path ? thread.parent : null;
64
+ if (thread) return thread.parent && thread.parent !== path ? thread.parent : null;
71
65
  return lexicalParent(path);
72
66
  };
73
67
  const ensure = (path: string, seen = new Set<string>()): void => {