@jwilger/pi-development-system 0.50.0 → 0.52.0

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,4 +1,5 @@
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 settings-file loader with no tests of its own; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ // biome-ignore-all lint/suspicious/noShadow: vendored upstream settings-file loader with no tests of its own; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
3
  import { randomUUID } from "node:crypto";
3
4
  import {
4
5
  closeSync,
@@ -23,11 +24,7 @@ export type WidgetMode = (typeof WIDGET_MODES)[number];
23
24
  const TOOL_FILTERING_MODES = ["allowed", "all-except-blocked", "all"] as const;
24
25
  export type ToolFilteringMode = (typeof TOOL_FILTERING_MODES)[number];
25
26
 
26
- const MODEL_SELECTION_MODES = [
27
- "pick-first-available",
28
- "pick-first-scoped",
29
- "use-current",
30
- ] as const;
27
+ const MODEL_SELECTION_MODES = ["pick-first-available", "pick-first-scoped", "use-current"] as const;
31
28
  export type ModelSelectionMode = (typeof MODEL_SELECTION_MODES)[number];
32
29
 
33
30
  export interface ManagerSettings {
@@ -55,7 +52,15 @@ export const DEFAULT_MANAGER_SETTINGS: ManagerSettings = {
55
52
  };
56
53
 
57
54
  const KEYS = [
58
- "maxLevels", "maxConcurrent", "maxThreads", "modelSelection", "subagentMode", "toolFiltering", "widgetMode", "nerdFontIcons", "finalRecap",
55
+ "maxLevels",
56
+ "maxConcurrent",
57
+ "maxThreads",
58
+ "modelSelection",
59
+ "subagentMode",
60
+ "toolFiltering",
61
+ "widgetMode",
62
+ "nerdFontIcons",
63
+ "finalRecap",
59
64
  ] as const;
60
65
  const MAX_LEVELS = 32;
61
66
 
@@ -65,9 +70,7 @@ function errorCode(error: unknown): string | undefined {
65
70
  : undefined;
66
71
  }
67
72
 
68
- function lstatIfPresent(
69
- path: string,
70
- ): ReturnType<typeof lstatSync> | undefined {
73
+ function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | undefined {
71
74
  try {
72
75
  return lstatSync(path);
73
76
  } catch (error) {
@@ -76,9 +79,7 @@ function lstatIfPresent(
76
79
  }
77
80
  }
78
81
 
79
- function assertNotSymlinkPath(
80
- path: string,
81
- ): ReturnType<typeof lstatSync> | undefined {
82
+ function assertNotSymlinkPath(path: string): ReturnType<typeof lstatSync> | undefined {
82
83
  const stat = lstatIfPresent(path);
83
84
  if (stat?.isSymbolicLink()) throw new Error(`Unsafe symlink path: ${path}`);
84
85
  return stat;
@@ -107,8 +108,8 @@ function parseSettings(content: string): Partial<ManagerSettings> {
107
108
  try {
108
109
  parsed = JSON.parse(content);
109
110
  } catch (error) {
110
- const detail = error instanceof Error ? error.message : String(error);
111
- throw new Error(`Invalid JSON: ${detail}`);
111
+ const reason = error instanceof Error ? error.message : String(error);
112
+ throw new Error(`Invalid JSON: ${reason}`, { cause: error });
112
113
  }
113
114
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
114
115
  throw new Error("Settings must be a JSON object");
@@ -123,7 +124,9 @@ function parseSettings(content: string): Partial<ManagerSettings> {
123
124
  if (typeof record.scopedModelFiltering !== "boolean")
124
125
  throw new Error("scopedModelFiltering must be a boolean");
125
126
  if (!Object.hasOwn(record, "modelSelection"))
126
- layer.modelSelection = record.scopedModelFiltering ? "pick-first-scoped" : "pick-first-available";
127
+ layer.modelSelection = record.scopedModelFiltering
128
+ ? "pick-first-scoped"
129
+ : "pick-first-available";
127
130
  }
128
131
  for (const key of KEYS) {
129
132
  if (!Object.hasOwn(record, key)) continue;
@@ -186,8 +189,7 @@ function readLayerContent(entry: SettingsLayer): string | undefined {
186
189
  }
187
190
  const stat = assertNotSymlinkPath(entry.filePath);
188
191
  if (!stat) return undefined;
189
- if (!stat.isFile())
190
- throw new Error(`Settings must be a regular file: ${entry.filePath}`);
192
+ if (!stat.isFile()) throw new Error(`Settings must be a regular file: ${entry.filePath}`);
191
193
  return readFileSync(entry.filePath, "utf8");
192
194
  }
193
195
 
@@ -214,9 +216,13 @@ export function loadManagerSaveScope(options: {
214
216
  if (content !== undefined) {
215
217
  const state: unknown = JSON.parse(content);
216
218
  if (
217
- state && typeof state === "object" && !Array.isArray(state) &&
218
- "saveScope" in state && state.saveScope === "user"
219
- ) return "user";
219
+ state &&
220
+ typeof state === "object" &&
221
+ !Array.isArray(state) &&
222
+ "saveScope" in state &&
223
+ state.saveScope === "user"
224
+ )
225
+ return "user";
220
226
  }
221
227
  } catch {
222
228
  // Missing, invalid or unsafe UI state must not prevent opening settings.
@@ -243,8 +249,7 @@ export function loadManagerSettings(options: {
243
249
  const diagnostics: string[] = [];
244
250
  const settings: ManagerSettings = { ...DEFAULT_MANAGER_SETTINGS };
245
251
  const layers = [layer(options.agentDir, ["subagent-manager"])];
246
- if (options.includeProject)
247
- layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
252
+ if (options.includeProject) layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
248
253
  for (const entry of layers) {
249
254
  try {
250
255
  const parsed = readLayer(entry);
@@ -257,13 +262,6 @@ export function loadManagerSettings(options: {
257
262
  }
258
263
 
259
264
  function serializedSettings(settings: ManagerSettings): string {
260
- if (
261
- settings === null ||
262
- typeof settings !== "object" ||
263
- Array.isArray(settings)
264
- ) {
265
- throw new Error("Settings must be a JSON object");
266
- }
267
265
  for (const key of Object.keys(settings)) {
268
266
  if (!isSupportedKey(key)) throw new Error(`Unknown settings key: ${key}`);
269
267
  }
@@ -298,17 +296,20 @@ function ensureOwnedDirectory(directory: string): void {
298
296
  }
299
297
 
300
298
  function ensureScopeDirectories(directories: string[]): void {
301
- assertExistingDirectory(directories[0]);
302
- for (let index = 1; index < directories.length; index++) {
303
- assertExistingDirectory(directories[index - 1]);
304
- ensureOwnedDirectory(directories[index]);
299
+ const [root, ...rest] = directories;
300
+ if (root === undefined) return;
301
+ assertExistingDirectory(root);
302
+ let parent = root;
303
+ for (const directory of rest) {
304
+ assertExistingDirectory(parent);
305
+ ensureOwnedDirectory(directory);
306
+ parent = directory;
305
307
  }
306
308
  }
307
309
 
308
310
  function assertRegularFileOrAbsent(filePath: string): void {
309
311
  const stat = assertNotSymlinkPath(filePath);
310
- if (stat && !stat.isFile())
311
- throw new Error(`Settings must be a regular file: ${filePath}`);
312
+ if (stat && !stat.isFile()) throw new Error(`Settings must be a regular file: ${filePath}`);
312
313
  }
313
314
 
314
315
  function exclusiveCreateFlags(): number {
@@ -333,8 +334,7 @@ function writeAtomically(entry: SettingsLayer, content: string): void {
333
334
  for (const directory of entry.directories) assertExistingDirectory(directory);
334
335
  assertRegularFileOrAbsent(filePath);
335
336
  const tempStat = assertNotSymlinkPath(temporary);
336
- if (!tempStat?.isFile())
337
- throw new Error(`Settings must be a regular file: ${temporary}`);
337
+ if (!tempStat?.isFile()) throw new Error(`Settings must be a regular file: ${temporary}`);
338
338
  renameSync(temporary, filePath);
339
339
  } finally {
340
340
  if (created) {
@@ -1,4 +1,3 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
2
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
3
 
@@ -11,22 +10,24 @@ export interface ResolvedAgentSettings {
11
10
  export interface AgentType {
12
11
  name: string;
13
12
  description: string;
14
- models?: string[];
13
+ models?: string[] | undefined;
15
14
  /** @deprecated Use ordered models instead. */
16
- model?: string;
15
+ model?: string | undefined;
17
16
  /** Advisory display names. Not provider/model pins and never used for selection. */
18
- modelSuggestions?: string[];
19
- thinkingLevel?: ThinkingLevel;
17
+ modelSuggestions?: string[] | undefined;
18
+ thinkingLevel?: ThinkingLevel | undefined;
20
19
  /** devsys: per-spawn pin set by the coordinator via agent_spawn; beats preferences and inheritance. */
21
- spawnOverrides?: { model?: string; thinkingLevel?: ThinkingLevel };
20
+ spawnOverrides?:
21
+ | { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined }
22
+ | undefined;
22
23
  /** Pi semantic token for the type pill background and task path foreground. */
23
- color?: string | undefined;
24
+ color?: string | undefined | undefined;
24
25
  /** Optional literal Nerd Font glyph; displayed only when the labs setting is enabled. */
25
- icon?: string | undefined;
26
- tools?: { allow?: string[]; block?: string[] };
26
+ icon?: string | undefined | undefined;
27
+ tools?: { allow?: string[] | undefined; block?: string[] | undefined } | undefined;
27
28
  systemPrompt: string;
28
- filePath?: string;
29
- source?: "bundled" | "user" | "project";
29
+ filePath?: string | undefined;
30
+ source?: "bundled" | "user" | "project" | undefined;
30
31
  /**
31
32
  * How this definition is customized relative to its base.
32
33
  * - fork: a full `<name>.md` copy (owns the system prompt).
@@ -34,15 +35,17 @@ export interface AgentType {
34
35
  * (no Markdown body; unset fields follow the base).
35
36
  * Absent for pure bundled definitions and brand-new drafts.
36
37
  */
37
- customization?: {
38
- kind: "fork" | "override";
39
- scope: "user" | "project";
40
- filePath: string;
41
- };
38
+ customization?:
39
+ | {
40
+ kind: "fork" | "override";
41
+ scope: "user" | "project";
42
+ filePath: string;
43
+ }
44
+ | undefined;
42
45
  /** Source of the base definition under the customization, if any. */
43
- baseSource?: "bundled" | "user" | "project";
46
+ baseSource?: "bundled" | "user" | "project" | undefined;
44
47
  /** File path of the base definition under the customization, if any. */
45
- baseFilePath?: string;
48
+ baseFilePath?: string | undefined;
46
49
  }
47
50
  type ThreadState = "starting" | "running" | "paused" | "completed" | "failed" | "stopped";
48
51
  export interface ThreadView {
@@ -69,7 +72,7 @@ export interface ThreadView {
69
72
  /** Cumulative output tokens. Live views include the in-progress message. */
70
73
  outputTokens?: number;
71
74
  /** devsys: per-spawn model/thinkingLevel pin requested through agent_spawn, when any. */
72
- pinned?: { model?: string; thinkingLevel?: ThinkingLevel };
75
+ pinned?: { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined };
73
76
  }
74
77
  type DriverEvent =
75
78
  | { kind: "activity" | "error"; text: string }
@@ -1,4 +1,9 @@
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/noVoid: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ // biome-ignore-all lint/correctness/noVoidTypeReturn: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
3
+ // biome-ignore-all lint/suspicious/noControlCharactersInRegex: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
4
+ // biome-ignore-all lint/suspicious/noEmptyBlockStatements: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
5
+ // biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
6
+ // biome-ignore-all lint/suspicious/useAwait: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
7
  import type {
3
8
  ExtensionContext,
4
9
  ExtensionUIContext,
@@ -6,6 +11,7 @@ import type {
6
11
  Theme,
7
12
  } from "@earendil-works/pi-coding-agent";
8
13
  import {
14
+ type Component,
9
15
  CURSOR_MARKER,
10
16
  Editor,
11
17
  getKeybindings,
@@ -13,11 +19,10 @@ import {
13
19
  Key,
14
20
  matchesKey,
15
21
  stripTerminalSequences,
22
+ type TUI,
16
23
  truncateToWidth,
17
24
  visibleWidth,
18
25
  wrapTextWithAnsi,
19
- type Component,
20
- type TUI,
21
26
  } from "@earendil-works/pi-tui";
22
27
 
23
28
  export interface DialogHost {
@@ -31,10 +36,10 @@ export interface DialogRow {
31
36
  id: string;
32
37
  label: string;
33
38
  /** Trusted formatter receives sanitized text; generated ANSI styling is kept when clipping. */
34
- renderLabel?: (label: string, theme: Theme) => string;
35
- value?: string;
36
- valueColor?: Parameters<Theme["fg"]>[0];
37
- help?: string;
39
+ renderLabel?: ((label: string, theme: Theme) => string) | undefined;
40
+ value?: string | undefined;
41
+ valueColor?: Parameters<Theme["fg"]>[0] | undefined;
42
+ help?: string | undefined;
38
43
  }
39
44
  export const DIALOG_OPTIONS = {
40
45
  overlay: true,
@@ -69,18 +74,14 @@ export function frameDialog(
69
74
  const clipped = truncateToWidth(text, Math.max(0, columns), "", true);
70
75
  return clipped + " ".repeat(Math.max(0, columns - visibleWidth(clipped)));
71
76
  };
72
- if (width < 4 || height < 4)
73
- return [fit(theme.fg("muted", dialogText(title)), width)];
77
+ if (width < 4 || height < 4) return [fit(theme.fg("muted", dialogText(title)), width)];
74
78
  const inner = width - 2;
75
79
  const line = (text: string) =>
76
80
  theme.fg("border", "│") + fit(text, inner) + theme.fg("border", "│");
77
81
  const heading = truncateToWidth(` ${dialogText(title)} `, inner, "");
78
82
  const top = theme.fg(
79
83
  "border",
80
- "╭" +
81
- heading +
82
- "─".repeat(Math.max(0, inner - visibleWidth(heading))) +
83
- "╮",
84
+ `╭${heading}${"─".repeat(Math.max(0, inner - visibleWidth(heading)))}╮`,
84
85
  );
85
86
  // Pad to the full frame so centered overlays keep the same bounds between views.
86
87
  const budget = height - 4;
@@ -90,9 +91,9 @@ export function frameDialog(
90
91
  return [
91
92
  top,
92
93
  ...content,
93
- theme.fg("border", "├" + "─".repeat(inner) + "┤"),
94
+ theme.fg("border", `├${"─".repeat(inner)}┤`),
94
95
  line(theme.fg("dim", dialogText(footer))),
95
- theme.fg("border", "╰" + "─".repeat(inner) + "╯"),
96
+ theme.fg("border", `╰${"─".repeat(inner)}╯`),
96
97
  ];
97
98
  }
98
99
 
@@ -100,13 +101,24 @@ export function frameDialog(
100
101
  class DialogEditor {
101
102
  private editor: Editor;
102
103
  private initialText: string;
104
+ private host: DialogEditorHost;
105
+ private theme: Theme;
106
+ readonly title: string;
107
+ readonly prefill: string;
108
+ private done: (value: string | undefined) => void;
109
+
103
110
  constructor(
104
- private host: DialogEditorHost,
105
- private theme: Theme,
106
- readonly title: string,
107
- readonly prefill: string,
108
- private done: (value: string | undefined) => void,
111
+ host: DialogEditorHost,
112
+ theme: Theme,
113
+ title: string,
114
+ prefill: string,
115
+ done: (value: string | undefined) => void,
109
116
  ) {
117
+ this.host = host;
118
+ this.theme = theme;
119
+ this.title = title;
120
+ this.prefill = prefill;
121
+ this.done = done;
110
122
  this.editor = new Editor(
111
123
  host as ConstructorParameters<typeof Editor>[0],
112
124
  {
@@ -173,8 +185,7 @@ export function dialogEditor(
173
185
  prefill: string,
174
186
  ): Promise<string | undefined> {
175
187
  return ctx.ui.custom(
176
- (host, theme, _keys, done) =>
177
- new DialogEditor(host, theme, title, prefill, done),
188
+ (host, theme, _keys, done) => new DialogEditor(host, theme, title, prefill, done),
178
189
  DIALOG_OPTIONS,
179
190
  );
180
191
  }
@@ -183,16 +194,31 @@ export function dialogEditor(
183
194
  class DialogMenu {
184
195
  private selected = 0;
185
196
  private viewport = 1;
197
+ private host: DialogHost;
198
+ private theme: Theme;
199
+ readonly title: string;
200
+ readonly rows: DialogRow[];
201
+ private done: (id: string | undefined) => void;
202
+ readonly footer: string;
203
+ private saveId: string | undefined;
204
+
186
205
  constructor(
187
- private host: DialogHost,
188
- private theme: Theme,
189
- readonly title: string,
190
- readonly rows: DialogRow[],
191
- private done: (id: string | undefined) => void,
206
+ host: DialogHost,
207
+ theme: Theme,
208
+ title: string,
209
+ rows: DialogRow[],
210
+ done: (id: string | undefined) => void,
192
211
  selectedId?: string,
193
- readonly footer = "↑↓ navigate · Enter select · Esc close",
194
- private saveId?: string,
212
+ footer: string = "↑↓ navigate · Enter select · Esc close",
213
+ saveId?: string,
195
214
  ) {
215
+ this.host = host;
216
+ this.theme = theme;
217
+ this.title = title;
218
+ this.rows = rows;
219
+ this.done = done;
220
+ this.footer = footer;
221
+ this.saveId = saveId;
196
222
  const index = rows.findIndex((row) => row.id === selectedId);
197
223
  if (index >= 0) this.selected = index;
198
224
  }
@@ -202,23 +228,16 @@ class DialogMenu {
202
228
  handleInput(data: string): void {
203
229
  const keys = getKeybindings();
204
230
  if (keys.matches(data, "tui.select.cancel")) return this.done(undefined);
205
- if (keys.matches(data, "tui.select.confirm"))
206
- return this.done(this.getSelectedId());
207
- if (this.saveId && matchesKey(data, Key.ctrl("s")))
208
- return this.done(this.saveId);
231
+ if (keys.matches(data, "tui.select.confirm")) return this.done(this.getSelectedId());
232
+ if (this.saveId && matchesKey(data, Key.ctrl("s"))) return this.done(this.saveId);
209
233
  let offset = 0;
210
234
  if (keys.matches(data, "tui.select.up")) offset = -1;
211
- else if (keys.matches(data, "tui.select.down") || matchesKey(data, Key.tab))
212
- offset = 1;
235
+ else if (keys.matches(data, "tui.select.down") || matchesKey(data, Key.tab)) offset = 1;
213
236
  else if (matchesKey(data, Key.pageUp)) offset = -this.viewport;
214
237
  else if (matchesKey(data, Key.pageDown)) offset = this.viewport;
215
238
  else if (matchesKey(data, Key.home)) this.selected = 0;
216
- else if (matchesKey(data, Key.end))
217
- this.selected = Math.max(0, this.rows.length - 1);
218
- this.selected = Math.max(
219
- 0,
220
- Math.min(this.rows.length - 1, this.selected + offset),
221
- );
239
+ else if (matchesKey(data, Key.end)) this.selected = Math.max(0, this.rows.length - 1);
240
+ this.selected = Math.max(0, Math.min(this.rows.length - 1, this.selected + offset));
222
241
  this.host.requestRender();
223
242
  }
224
243
  invalidate(): void {}
@@ -232,20 +251,17 @@ class DialogMenu {
232
251
  this.viewport = Math.max(1, height - 6 - help.length);
233
252
  const start = Math.max(
234
253
  0,
235
- Math.min(
236
- this.selected - this.viewport + 1,
237
- this.rows.length - this.viewport,
238
- ),
254
+ Math.min(this.selected - this.viewport + 1, this.rows.length - this.viewport),
239
255
  );
240
256
  const labelWidth = Math.min(28, Math.max(8, Math.floor(inner * 0.35)));
241
257
  const body = [
242
258
  this.theme.fg(
243
259
  "dim",
244
- ` ${this.rows.length ? this.selected + 1 : 0}/${this.rows.length} · Field / Value`,
260
+ ` ${this.rows.length > 0 ? this.selected + 1 : 0}/${this.rows.length} · Field / Value`,
245
261
  ),
246
262
  ...this.rows.slice(start, start + this.viewport).map((row, i) => {
247
263
  const selected = start + i === this.selected;
248
- const color = (text: string) => selected ? this.theme.fg("accent", text) : text;
264
+ const color = (text: string) => (selected ? this.theme.fg("accent", text) : text);
249
265
  const plainLabel = dialogText(row.label);
250
266
  const label = truncateToWidth(
251
267
  row.renderLabel ? row.renderLabel(plainLabel, this.theme) : plainLabel,
@@ -255,22 +271,17 @@ class DialogMenu {
255
271
  );
256
272
  const padding = " ".repeat(Math.max(0, labelWidth - visibleWidth(label)));
257
273
  const value = dialogText(row.value ?? "");
258
- return color(`${selected ? "›" : " "} `)
259
- + (row.renderLabel ? label : color(label))
260
- + color(`${padding} │ `)
261
- + (row.valueColor ? this.theme.fg(row.valueColor, value) : color(value));
274
+ return (
275
+ color(`${selected ? "›" : " "} `) +
276
+ (row.renderLabel ? label : color(label)) +
277
+ color(`${padding} │ `) +
278
+ (row.valueColor ? this.theme.fg(row.valueColor, value) : color(value))
279
+ );
262
280
  }),
263
281
  "",
264
282
  ...help.map((line) => this.theme.fg("muted", ` ${line}`)),
265
283
  ];
266
- return frameDialog(
267
- this.theme,
268
- width,
269
- height,
270
- this.title,
271
- body,
272
- this.footer,
273
- );
284
+ return frameDialog(this.theme, width, height, this.title, body, this.footer);
274
285
  }
275
286
  }
276
287
 
@@ -287,7 +298,11 @@ export async function dialogMenu(
287
298
  ctx: ExtensionContext,
288
299
  title: string,
289
300
  rows: DialogRow[],
290
- options: { selectedId?: string; footer?: string; saveId?: string } = {},
301
+ options: {
302
+ selectedId?: string | undefined;
303
+ footer?: string | undefined;
304
+ saveId?: string | undefined;
305
+ } = {},
291
306
  ): Promise<string | undefined> {
292
307
  return ctx.ui.custom(
293
308
  (host, theme, _keys, done) =>
@@ -371,9 +386,7 @@ const dialogScopes = new WeakSet<ExtensionContext>();
371
386
 
372
387
  function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
373
388
  return (
374
- typeof value === "object" &&
375
- value !== null &&
376
- typeof (value as Promise<T>).then === "function"
389
+ typeof value === "object" && value !== null && typeof (value as Promise<T>).then === "function"
377
390
  );
378
391
  }
379
392
 
@@ -395,14 +408,25 @@ export class DialogSession {
395
408
  private disposed = false;
396
409
  private generation = 0;
397
410
  private pending: PendingView | undefined;
398
- private outerDone: ((result?: void) => void) | undefined;
411
+ private outerDone: ((result?: undefined) => void) | undefined;
412
+
413
+ private host: TUI;
414
+
415
+ private theme: Theme;
416
+
417
+ private keys: KeybindingsManager;
399
418
 
400
419
  constructor(
401
- private host: TUI,
402
- private theme: Theme,
403
- private keys: KeybindingsManager,
404
- done: (result?: void) => void,
420
+ host: TUI,
421
+ theme: Theme,
422
+ keys: KeybindingsManager,
423
+ done: (result?: undefined) => void,
405
424
  ) {
425
+ this.host = host;
426
+
427
+ this.theme = theme;
428
+
429
+ this.keys = keys;
406
430
  this.outerDone = done;
407
431
  }
408
432
 
@@ -442,16 +466,14 @@ export class DialogSession {
442
466
  const resolveOnce = (value: T) => {
443
467
  if (settled) return;
444
468
  settled = true;
445
- if (this.pending?.generation === generation)
446
- this.pending.settled = true;
469
+ if (this.pending?.generation === generation) this.pending.settled = true;
447
470
  resolve(value);
448
471
  };
449
472
  const rejectOnce = (error: unknown) => {
450
473
  if (settled) return;
451
474
  settled = true;
452
475
  this.inputLocked = true;
453
- if (this.pending?.generation === generation)
454
- this.pending.settled = true;
476
+ if (this.pending?.generation === generation) this.pending.settled = true;
455
477
  reject(error);
456
478
  };
457
479
  this.pending = {
@@ -480,8 +502,7 @@ export class DialogSession {
480
502
  void produced.then(
481
503
  (component) => adopt(component),
482
504
  (error: unknown) => {
483
- if (settled || this.closed || generation !== this.generation)
484
- return;
505
+ if (settled || this.closed || generation !== this.generation) return;
485
506
  rejectOnce(error);
486
507
  },
487
508
  );
@@ -540,8 +561,7 @@ export class DialogSession {
540
561
  return;
541
562
  }
542
563
  this.replaceChild(component);
543
- this.inputLocked =
544
- this.pending?.generation === generation ? this.pending.settled : true;
564
+ this.inputLocked = this.pending?.generation === generation ? this.pending.settled : true;
545
565
  this.host.requestRender();
546
566
  }
547
567
 
@@ -556,7 +576,7 @@ export class DialogSession {
556
576
 
557
577
  private forwardFocus(): void {
558
578
  const child = this.child;
559
- if (!child || !("focused" in child)) return;
579
+ if (!(child && "focused" in child)) return;
560
580
  // Read the setter's latest value; the first view can mount before showOverlay focuses us.
561
581
  child.focused = this.focused;
562
582
  }
@@ -569,18 +589,13 @@ export class DialogSession {
569
589
  }
570
590
  }
571
591
 
572
- export function scopeDialogContext<T extends ExtensionContext>(
573
- ctx: T,
574
- session: DialogSession,
575
- ): T {
592
+ export function scopeDialogContext<T extends ExtensionContext>(ctx: T, session: DialogSession): T {
576
593
  const source = ctx.ui;
577
594
  const originalCustom = source.custom.bind(source);
578
595
  const ui = Object.create(source) as ExtensionUIContext;
579
596
  ui.custom = ((factory, options) => {
580
597
  if (options?.overlay) return session.mount(factory);
581
- return originalCustom(factory, options).finally(() =>
582
- session.restoreFocus(),
583
- );
598
+ return originalCustom(factory, options).finally(() => session.restoreFocus());
584
599
  }) as ExtensionUIContext["custom"];
585
600
  // Pi exposes ctx.ui as a getter; assignment cannot shadow an inherited accessor.
586
601
  // Define an own property while keeping the remaining context getters live.
@@ -1,4 +1,3 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
1
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
2
  import {
4
3
  CURSOR_MARKER,
@@ -10,10 +9,10 @@ import {
10
9
  } from "@earendil-works/pi-tui";
11
10
  import {
12
11
  DIALOG_OPTIONS,
12
+ type DialogHost,
13
13
  dialogHeight,
14
14
  dialogText,
15
15
  frameDialog,
16
- type DialogHost,
17
16
  } from "./dialog.ts";
18
17
 
19
18
  export interface ImportPickerItem {
@@ -76,12 +75,28 @@ class ImportPicker {
76
75
  private viewport = 1;
77
76
  private readonly checked = new Set<string>();
78
77
 
78
+ private host: DialogHost;
79
+
80
+ private theme: Theme;
81
+
82
+ private readonly items: readonly ImportPickerItem[];
83
+
84
+ private done: (ids: string[] | undefined) => void;
85
+
79
86
  constructor(
80
- private host: DialogHost,
81
- private theme: Theme,
82
- private readonly items: readonly ImportPickerItem[],
83
- private done: (ids: string[] | undefined) => void,
84
- ) {}
87
+ host: DialogHost,
88
+ theme: Theme,
89
+ items: readonly ImportPickerItem[],
90
+ done: (ids: string[] | undefined) => void,
91
+ ) {
92
+ this.host = host;
93
+
94
+ this.theme = theme;
95
+
96
+ this.items = items;
97
+
98
+ this.done = done;
99
+ }
85
100
 
86
101
  /** Selected ids in source order. Empty until the user toggles a row. */
87
102
  getSelectedIds(): string[] {
@@ -99,7 +114,9 @@ class ImportPicker {
99
114
  return this.cursor;
100
115
  }
101
116
 
102
- invalidate(): void {}
117
+ invalidate(): void {
118
+ // Nothing is cached, so there is nothing to invalidate.
119
+ }
103
120
 
104
121
  handleInput(data: string): void {
105
122
  const keys = getKeybindings();
@@ -155,8 +172,8 @@ class ImportPicker {
155
172
  if (listCount > 0) body.push(this.theme.fg("muted", " No external agents to import"));
156
173
  } else {
157
174
  const rows = this.items.slice(start, start + listCount);
158
- for (let offset = 0; offset < rows.length; offset++)
159
- body.push(this.row(rows[offset]!, start + offset === this.cursor));
175
+ for (const [offset, row] of rows.entries())
176
+ body.push(this.row(row, start + offset === this.cursor));
160
177
  }
161
178
  if (gap) body.push("");
162
179
  for (const line of detail.slice(0, detailCount)) body.push(this.theme.fg("muted", ` ${line}`));
@@ -164,7 +181,7 @@ class ImportPicker {
164
181
  }
165
182
 
166
183
  private countLine(start: number, shown: number): string {
167
- const position = this.items.length ? this.cursor + 1 : 0;
184
+ const position = this.items.length > 0 ? this.cursor + 1 : 0;
168
185
  return this.theme.fg(
169
186
  "dim",
170
187
  ` ${this.scrollHint(start, shown)}${this.checked.size} selected · ${position}/${this.items.length}`,