@jwilger/pi-development-system 0.51.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -65,7 +65,7 @@ export type ResultLike = {
65
65
  const NO_TEST_SCRIPT = /Missing script: "?test/;
66
66
  /** What npm prints around a missing script; any other line means something else ran or failed. */
67
67
  const NPM_MISSING_SCRIPT_NOISE =
68
- /^(?:npm (?:error|ERR!)\s*)?(?:$|.*Missing script.*|.*To see a list of scripts.*|.*A complete log of this run.*|.*Did you mean.*|.*npm run.*)$/;
68
+ /^(?:npm (?:error|ERR!)\s*)?(?:$|.*Missing script.*|.*To see a list of scripts.*|.*A complete log of this run.*|.*Did you mean.*|.*npm run.*|Command exited with code \d+)$/;
69
69
  const onlyMissingScript = (text: string): boolean =>
70
70
  NO_TEST_SCRIPT.test(text) &&
71
71
  text.split("\n").every((l) => NPM_MISSING_SCRIPT_NOISE.test(l.trim()));
@@ -1,25 +1,21 @@
1
1
  // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
2
  import { randomUUID } from "node:crypto";
3
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
3
  import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { fuzzyFilter } from "@earendil-works/pi-tui";
6
- import { createInheritedToolSource } from "./orch/inherited-tools.ts";
7
- import { ThreadManager } from "./orch/manager.ts";
8
- import { subagentPrompt } from "./orch/prompt.ts";
9
- import { createDriverFactory } from "./orch/runtime.ts";
10
- import { agentTools } from "./orch/tools.ts";
11
- import {
12
- IMPORT_REQUEST_PREFIX,
13
- markImportOffered,
14
- offerAgentImport,
15
- } from "./prefs/agent-import.ts";
16
6
  import { ConfigStore } from "./prefs/config.ts";
7
+ import { IMPORT_REQUEST_PREFIX, markImportOffered, offerAgentImport } from "./prefs/agent-import.ts";
8
+ import { ThreadManager } from "./orch/manager.ts";
17
9
  import { loadManagerSettings } from "./prefs/settings.ts";
18
- import type { SavedThread, ThreadEvent } from "./types.ts";
19
- import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
20
10
  import { configureAgents } from "./ui/settings-ui.ts";
21
11
  import { AgentNavigationController, showAgentStatus } from "./ui/status-ui.ts";
12
+ import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
13
+ import { createDriverFactory } from "./orch/runtime.ts";
14
+ import { createInheritedToolSource } from "./orch/inherited-tools.ts";
15
+ import { subagentPrompt } from "./orch/prompt.ts";
16
+ import { agentTools } from "./orch/tools.ts";
22
17
  import { editAgentTypes, updateWidget } from "./ui/ui.ts";
18
+ import type { SavedThread, ThreadEvent } from "./types.ts";
23
19
 
24
20
  const REGISTRY_ENTRY = "pi-subagent:registry:v1";
25
21
  const ROOT_MAILBOX_ENTRY = "pi-subagent:root-mailbox:v1";
@@ -85,55 +81,32 @@ export default function piSubagent(pi: ExtensionAPI): void {
85
81
  if (existingFactory && existingFactory !== navigationEditorFactory) return;
86
82
  // The host only transfers raw text, not cursor/undo/expanded-paste state.
87
83
  if (ctx.ui.getEditorText?.()) return;
88
- const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
89
- (message) => {
90
- if (message.role !== "user") return [];
91
- const content = message.content;
92
- return [
93
- typeof content === "string"
94
- ? content
95
- : content
96
- .filter((block) => block.type === "text")
97
- .map((block) => block.text)
98
- .join(""),
99
- ];
100
- },
101
- );
84
+ const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap((message) => {
85
+ if (message.role !== "user") return [];
86
+ const content = message.content;
87
+ return [typeof content === "string" ? content : content
88
+ .filter((block) => block.type === "text").map((block) => block.text).join("")];
89
+ });
102
90
  navigationEditorFactory = (tui, theme, keys) => {
103
91
  const editor = new AgentNavigationEditor(tui, theme, keys, {
104
92
  // Initial startup hydrates after session_start; replacement editors do not.
105
93
  initialHistory: seedHistory ? history : undefined,
106
94
  generation: () => navigationGeneration,
107
- canOpen: () =>
108
- manager !== undefined &&
109
- context?.mode === "tui" &&
110
- limits.subagentMode !== "off" &&
111
- !navigation.isOpen &&
112
- manager
113
- .list()
114
- .some(
115
- (thread) =>
116
- thread.path !== "/root" &&
117
- (thread.state === "starting" || thread.state === "running"),
118
- ),
119
- openTree: () =>
120
- manager && context
121
- ? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons)
122
- : undefined,
123
- canCollapse: () =>
124
- manager !== undefined &&
125
- context?.mode === "tui" &&
126
- limits.subagentMode !== "off" &&
127
- !navigation.isOpen &&
128
- !widgetCollapsed &&
129
- limits.widgetMode === "full" &&
130
- widgetThreads().some((thread) => thread.path !== "/root"),
95
+ canOpen: () => manager !== undefined && context?.mode === "tui" &&
96
+ limits.subagentMode !== "off" && !navigation.isOpen &&
97
+ manager.list().some((thread) => thread.path !== "/root" &&
98
+ (thread.state === "starting" || thread.state === "running")),
99
+ openTree: () => manager && context
100
+ ? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons) : undefined,
101
+ canCollapse: () => manager !== undefined && context?.mode === "tui" &&
102
+ limits.subagentMode !== "off" && !navigation.isOpen && !widgetCollapsed &&
103
+ limits.widgetMode === "full" && widgetThreads().some((thread) => thread.path !== "/root"),
131
104
  collapseWidget: () => {
132
105
  widgetCollapsed = true;
133
106
  if (context) refreshWidget(context);
134
107
  },
135
- onError: (error) =>
136
- context?.ui.notify(error instanceof Error ? error.message : String(error), "error"),
108
+ onError: (error) => context?.ui.notify(
109
+ error instanceof Error ? error.message : String(error), "error"),
137
110
  });
138
111
  navigationEditor = editor;
139
112
  return editor;
@@ -147,10 +120,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
147
120
  // Migration requires the root's file tools and must not delegate source prompts.
148
121
  if (limits.subagentMode === "orchestration") {
149
122
  if (!firstRun)
150
- ctx.ui.notify(
151
- "Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.",
152
- "info",
153
- );
123
+ ctx.ui.notify("Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.", "info");
154
124
  return;
155
125
  }
156
126
  if (importInProgress) return;
@@ -170,15 +140,12 @@ export default function piSubagent(pi: ExtensionAPI): void {
170
140
  if (!manager) throw new Error("Subagent threads are not initialized; start a Pi session first");
171
141
  return manager;
172
142
  };
173
- const widgetThreads = () =>
174
- requireManager()
175
- .list()
176
- .filter((thread) => {
177
- // Resuming a retained session makes it visible for this task, even after it settles again.
178
- if (thread.state === "starting" || thread.state === "running")
179
- hiddenWidgetThreads.delete(thread.path);
180
- return !hiddenWidgetThreads.has(thread.path);
181
- });
143
+ const widgetThreads = () => requireManager().list().filter((thread) => {
144
+ // Resuming a retained session makes it visible for this task, even after it settles again.
145
+ if (thread.state === "starting" || thread.state === "running")
146
+ hiddenWidgetThreads.delete(thread.path);
147
+ return !hiddenWidgetThreads.has(thread.path);
148
+ });
182
149
  const requireContext = () => {
183
150
  if (!context) throw new Error("No active Pi session");
184
151
  return context;
@@ -220,72 +187,44 @@ export default function piSubagent(pi: ExtensionAPI): void {
220
187
  };
221
188
  const pendingSummaries = (ctx: ExtensionContext) => {
222
189
  const entries = ctx.sessionManager.getBranch();
223
- const summarized = new Set(
224
- entries.flatMap((entry) => {
225
- if (entry.type !== "custom_message" || entry.customType !== ROOT_SUMMARY_MESSAGE) return [];
226
- const ids = (entry.details as { mailboxIds?: unknown } | undefined)?.mailboxIds;
227
- return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
228
- }),
229
- );
190
+ const summarized = new Set(entries.flatMap((entry) => {
191
+ if (entry.type !== "custom_message" || entry.customType !== ROOT_SUMMARY_MESSAGE) return [];
192
+ const ids = (entry.details as { mailboxIds?: unknown } | undefined)?.mailboxIds;
193
+ return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
194
+ }));
230
195
  return entries.flatMap((entry) => {
231
196
  if (entry.type !== "custom" || entry.customType !== ROOT_MAILBOX_ENTRY) return [];
232
197
  const notification = entry.data as RootNotification | undefined;
233
198
  return notification?.rootSessionId === ctx.sessionManager.getSessionId() &&
234
- notification.finalRecap === true &&
235
- typeof notification.content === "string" &&
199
+ notification.finalRecap === true && typeof notification.content === "string" &&
236
200
  typeof notification.details?.mailboxId === "string" &&
237
- !summarized.has(notification.details.mailboxId)
238
- ? [notification]
239
- : [];
201
+ !summarized.has(notification.details.mailboxId) ? [notification] : [];
240
202
  });
241
203
  };
242
204
  const scheduleSummary = (ctx: ExtensionContext) => {
243
- if (
244
- !limits.finalRecap ||
245
- limits.subagentMode === "off" ||
246
- summaryRunning ||
247
- suspendingSummaries ||
248
- summaryTimer !== undefined ||
249
- ctx.isIdle() === false ||
250
- !pendingSummaries(ctx).length
251
- )
252
- return;
205
+ if (!limits.finalRecap || limits.subagentMode === "off" || summaryRunning || suspendingSummaries ||
206
+ summaryTimer !== undefined || ctx.isIdle() === false || !pendingSummaries(ctx).length) return;
253
207
  const currentGeneration = generation;
254
208
  // Coalesce siblings finishing together and leave the settlement boundary before
255
209
  // starting a new turn. Recheck settings/session/idle state after the deferral.
256
210
  summaryTimer = setTimeout(() => {
257
211
  summaryTimer = undefined;
258
- if (
259
- currentGeneration !== generation ||
260
- context?.sessionManager !== ctx.sessionManager ||
261
- !limits.finalRecap ||
262
- limits.subagentMode === "off" ||
263
- suspendingSummaries ||
264
- ctx.isIdle() === false
265
- )
266
- return;
212
+ if (currentGeneration !== generation || context?.sessionManager !== ctx.sessionManager ||
213
+ !limits.finalRecap || limits.subagentMode === "off" || suspendingSummaries || ctx.isIdle() === false) return;
267
214
  const notifications = pendingSummaries(ctx);
268
215
  if (!notifications.length) return;
269
216
  summaryRunning = true;
270
217
  try {
271
- pi.sendMessage(
272
- {
273
- customType: ROOT_SUMMARY_MESSAGE,
274
- content:
275
- "Summarize the newly finished asynchronous subagent results for the user. " +
276
- "Their results are in the preceding subagent notifications; use agent_output if more detail is needed. " +
277
- "Give a concise main-thread answer covering results and any failures. " +
278
- "Do not spawn or resume agents solely to summarize.\nAgents: " +
279
- [...new Set(notifications.map((notification) => notification.details.path))].join(
280
- ", ",
281
- ),
282
- display: false,
283
- details: {
284
- mailboxIds: notifications.map((notification) => notification.details.mailboxId),
285
- },
286
- },
287
- { triggerTurn: true },
288
- );
218
+ pi.sendMessage({
219
+ customType: ROOT_SUMMARY_MESSAGE,
220
+ content: "Summarize the newly finished asynchronous subagent results for the user. " +
221
+ "Their results are in the preceding subagent notifications; use agent_output if more detail is needed. " +
222
+ "Give a concise main-thread answer covering results and any failures. " +
223
+ "Do not spawn or resume agents solely to summarize.\nAgents: " +
224
+ [...new Set(notifications.map((notification) => notification.details.path))].join(", "),
225
+ display: false,
226
+ details: { mailboxIds: notifications.map((notification) => notification.details.mailboxId) },
227
+ }, { triggerTurn: true });
289
228
  } catch (error) {
290
229
  summaryRunning = false;
291
230
  warnDelivery(error);
@@ -339,10 +278,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
339
278
  content: message,
340
279
  // Foreground results already handled by a busy main thread do not need
341
280
  // another turn. Completions during a summary do need a follow-up summary.
342
- finalRecap:
343
- limits.finalRecap &&
344
- !suspendingSummaries &&
345
- event.kind === "settled" &&
281
+ finalRecap: limits.finalRecap && !suspendingSummaries && event.kind === "settled" &&
346
282
  (thread.state === "completed" || thread.state === "failed") &&
347
283
  (requireContext().isIdle() || summaryRunning),
348
284
  details: {
@@ -399,14 +335,8 @@ export default function piSubagent(pi: ExtensionAPI): void {
399
335
  const refreshWidget = (ctx: ExtensionContext) => {
400
336
  if (limits.subagentMode === "off") {
401
337
  if (ctx.hasUI) ctx.ui.setWidget("pi-subagent", undefined);
402
- } else
403
- updateWidget(
404
- ctx,
405
- widgetThreads(),
406
- widgetCollapsed ? "minimal" : limits.widgetMode,
407
- limits.nerdFontIcons,
408
- rootTurnEnded,
409
- );
338
+ } else updateWidget(ctx, widgetThreads(), widgetCollapsed ? "minimal" : limits.widgetMode,
339
+ limits.nerdFontIcons, rootTurnEnded);
410
340
  };
411
341
  pi.on("agent_start", async (_event, ctx) => {
412
342
  // A new task must not resurrect the previous task's settled agents. Automatic
@@ -444,11 +374,7 @@ export default function piSubagent(pi: ExtensionAPI): void {
444
374
  };
445
375
  });
446
376
 
447
- const attachSession = async (
448
- ctx: ExtensionContext,
449
- installEditor = false,
450
- seedHistory = true,
451
- ) => {
377
+ const attachSession = async (ctx: ExtensionContext, installEditor = false, seedHistory = true) => {
452
378
  cancelSummary();
453
379
  const token = ++generation;
454
380
  closeNavigation(installEditor);
@@ -605,19 +531,9 @@ export default function piSubagent(pi: ExtensionAPI): void {
605
531
  },
606
532
  });
607
533
  } else if (command === "status") {
608
- await showAgentStatus(
609
- ctx,
610
- requireManager().scope("/root"),
611
- undefined,
612
- limits.nerdFontIcons,
613
- );
534
+ await showAgentStatus(ctx, requireManager().scope("/root"), undefined, limits.nerdFontIcons);
614
535
  } else if (command === "tree") {
615
- await navigation.open(
616
- ctx,
617
- requireManager().scope("/root"),
618
- rest.join(" ") || undefined,
619
- limits.nerdFontIcons,
620
- );
536
+ await navigation.open(ctx, requireManager().scope("/root"), rest.join(" ") || undefined, limits.nerdFontIcons);
621
537
  } else
622
538
  ctx.ui.notify(
623
539
  "Usage: /agents [tree [path] | status | settings | types | import | reload]",
@@ -43,7 +43,7 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
43
43
  for (const tool of loadout.registered) {
44
44
  const exposure = loadout.getExposure(tool.name);
45
45
  // Inactive direct tools and hidden tools are not available in the root either.
46
- if (!(callable.has(tool.name) || active.has(tool.name))) continue;
46
+ if (!callable.has(tool.name) && !active.has(tool.name)) continue;
47
47
  if (exposure === "hidden") continue;
48
48
  const metadata = info.get(tool.name);
49
49
  tools.push({
@@ -64,14 +64,14 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
64
64
  // SDK orchestrators are instead recreated locally by runtime.
65
65
  throw new Error(
66
66
  `Cannot safely inherit model-only tool ${tool.name}: the SDK does not ` +
67
- "provide a hook-preserving execution bridge. Run it in the main session.",
67
+ "provide a hook-preserving execution bridge. Run it in the main session.",
68
68
  );
69
69
  }
70
70
  const bridge = rootContext ?? context;
71
71
  if (!bridge) {
72
72
  throw new Error(
73
73
  "Inherited tool execution needs a main-session agent_* tool call first. " +
74
- "Use agent_steer to resume this child instead of the thread dialog.",
74
+ "Use agent_steer to resume this child instead of the thread dialog.",
75
75
  );
76
76
  }
77
77
  const outcome = await bridge.executeTool(tool.name, args, { signal, onUpdate });
@@ -1,4 +1,4 @@
1
- // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: the flagged functions (driver construction, settings resolution) are long sequences over pi sessions; only the pin and resume paths are covered by test/subagents/spawn-pins.test.ts, so splitting them would move untested behaviour (see src/subagents/VENDORED.md)
2
2
  import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, realpath } from "node:fs/promises";
4
4
  import path from "node:path";
@@ -159,8 +159,8 @@ export async function offerAgentImport(
159
159
  cwd: ctx.cwd,
160
160
  agentDir: options.agentDir,
161
161
  includeProject: ctx.isProjectTrusted(),
162
- homeDir: options.homeDir,
163
- extraAgentDirs: options.extraAgentDirs,
162
+ ...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
163
+ ...(options.extraAgentDirs === undefined ? {} : { extraAgentDirs: options.extraAgentDirs }),
164
164
  });
165
165
  if (found.diagnostics.length > 0)
166
166
  ctx.ui.notify(dialogText(found.diagnostics.join("\n")), "warning");
@@ -1,4 +1,4 @@
1
- // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: agent-file parsing and layer merging are long validators kept close to upstream; only parseAgentType is covered (test/agents/agents.test.ts), so splitting the rest would move untested behaviour (see src/subagents/VENDORED.md)
2
2
  import { randomUUID } from "node:crypto";
3
3
  import {
4
4
  lstatSync,
@@ -1,5 +1,5 @@
1
1
  // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
- import { type Dirent, lstatSync, readdirSync, readFileSync, type Stats } from "node:fs";
2
+ import { lstatSync, readdirSync, readFileSync, type Dirent, type Stats } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { parseDocument } from "yaml";
@@ -17,8 +17,8 @@ export interface DiscoverImportOptions {
17
17
  cwd: string;
18
18
  agentDir: string;
19
19
  includeProject: boolean;
20
- homeDir?: string | undefined;
21
- extraAgentDirs?: string | undefined;
20
+ homeDir?: string;
21
+ extraAgentDirs?: string;
22
22
  }
23
23
 
24
24
  const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
@@ -227,7 +227,7 @@ function walk(
227
227
  continue;
228
228
  }
229
229
  if (isSkillMarkdownName(entry.name)) continue;
230
- if (!(entry.isFile() && entry.name.endsWith(".md")) || entry.name.endsWith(".chain.md")) {
230
+ if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.endsWith(".chain.md")) {
231
231
  continue;
232
232
  }
233
233
  if (isExcluded(child, excludes) || isManagerOwned(child, managerDirs)) continue;
@@ -270,7 +270,7 @@ function considerFile(
270
270
  const rawName = fields.name?.trim() ?? "";
271
271
  const name = rawName || (permissive ? filenameName(resolved) : "");
272
272
  const description = fields.description ?? (permissive ? "" : undefined);
273
- if (!name || description === undefined || !(permissive || description.trim())) return;
273
+ if (!name || description === undefined || (!permissive && !description.trim())) return;
274
274
  const packageName = fields.packageName?.trim() ?? "";
275
275
  candidates.push({
276
276
  id: resolved,
@@ -303,7 +303,7 @@ function looseDisplayFields(content: string): DisplayFields | undefined {
303
303
  /* Ambiguous display metadata is not selectable. */
304
304
  }
305
305
  }
306
- if (!(fields.name && fields.description)) return undefined;
306
+ if (!fields.name || !fields.description) return undefined;
307
307
  return { name: fields.name, description: fields.description, packageName: fields.package };
308
308
  }
309
309
 
@@ -1,5 +1,5 @@
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/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (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)
3
3
  import { randomUUID } from "node:crypto";
4
4
  import {
5
5
  closeSync,
@@ -5,11 +5,11 @@ import {
5
5
  type KeybindingsManager,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import {
8
- type EditorTheme,
9
8
  getKeybindings,
10
9
  Key,
11
- type Keybinding,
12
10
  matchesKey,
11
+ type EditorTheme,
12
+ type Keybinding,
13
13
  type TUI,
14
14
  } from "@earendil-works/pi-tui";
15
15
 
@@ -87,11 +87,8 @@ export class AgentNavigationEditor extends CustomEditor {
87
87
 
88
88
  override handleInput(data: string): void {
89
89
  const outer = this.dispatchDepth === 0;
90
- const direction = this.ordinaryArrow(data, "left")
91
- ? "left"
92
- : this.ordinaryArrow(data, "right")
93
- ? "right"
94
- : undefined;
90
+ const direction = this.ordinaryArrow(data, "left") ? "left"
91
+ : this.ordinaryArrow(data, "right") ? "right" : undefined;
95
92
  if (outer && (!direction || (this.pending && this.pending !== direction))) this.inputEpoch++;
96
93
  const previousPaste = this.pastePending;
97
94
  const wasPaste = previousPaste || data.includes("\x1b[200~");
@@ -103,12 +100,10 @@ export class AgentNavigationEditor extends CustomEditor {
103
100
  const cursor = this.getCursor();
104
101
  const text = this.getText();
105
102
  const lines = text.split("\n");
106
- const atBoundary =
107
- direction === "left"
108
- ? cursor.line === 0 && cursor.col === 0
109
- : direction === "right" &&
110
- cursor.line === lines.length - 1 &&
111
- cursor.col === lines.at(-1)!.length;
103
+ const atBoundary = direction === "left"
104
+ ? cursor.line === 0 && cursor.col === 0
105
+ : direction === "right" && cursor.line === lines.length - 1 &&
106
+ cursor.col === lines.at(-1)!.length;
112
107
  const eligible =
113
108
  outer &&
114
109
  direction !== undefined &&
@@ -175,10 +170,9 @@ export class AgentNavigationEditor extends CustomEditor {
175
170
  generation: unknown,
176
171
  direction: "left" | "right",
177
172
  ): void {
178
- const allowed = () =>
179
- direction === "left"
180
- ? this.navigation.canOpen()
181
- : !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
173
+ const allowed = () => direction === "left"
174
+ ? this.navigation.canOpen()
175
+ : !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
182
176
  if (this.pending || !allowed()) return;
183
177
  this.pending = direction;
184
178
  const epoch = this.inputEpoch;
@@ -201,9 +195,8 @@ export class AgentNavigationEditor extends CustomEditor {
201
195
  try {
202
196
  // Call within the checked microtask: an extra Promise.then would leave
203
197
  // a gap in which root ownership could change after validation.
204
- void Promise.resolve(
205
- direction === "left" ? this.navigation.openTree() : this.navigation.collapseWidget!(),
206
- )
198
+ void Promise.resolve(direction === "left"
199
+ ? this.navigation.openTree() : this.navigation.collapseWidget!())
207
200
  .catch((error: unknown) => this.navigation.onError?.(error))
208
201
  .finally(() => {
209
202
  this.pending = undefined;
@@ -13,7 +13,7 @@ import {
13
13
  wrapTextWithAnsi,
14
14
  } from "@earendil-works/pi-tui";
15
15
  import type { ThreadService, TranscriptSnapshot } from "../types.ts";
16
- import { type DialogHost, dialogText } from "./dialog.ts";
16
+ import { dialogText, type DialogHost } from "./dialog.ts";
17
17
  import { AGENT_PROGRESS_INTERVAL, agentProgressIcon, agentTypeLabel } from "./ui.ts";
18
18
 
19
19
  /** Transcript detail level cycled by the t key: full → preview → compact. */
@@ -66,10 +66,7 @@ export class LiveAgentView {
66
66
  private unsubscribe: (() => void) | undefined;
67
67
  private renderTimer: ReturnType<typeof setTimeout> | undefined;
68
68
  private progressTimer: ReturnType<typeof setInterval> | undefined;
69
- private cache = new WeakMap<
70
- object,
71
- { width: number; detail: TranscriptDetail; lines: string[] }
72
- >();
69
+ private cache = new WeakMap<object, { width: number; detail: TranscriptDetail; lines: string[] }>();
73
70
  private scroll: ScrollView;
74
71
  private content: string[] = [];
75
72
  private restoreViewport = true;
@@ -191,10 +188,7 @@ export class LiveAgentView {
191
188
 
192
189
  private syncProgress(): void {
193
190
  const state = this.snapshot?.thread?.state;
194
- const active =
195
- this.nerdFontIcons &&
196
- !this.disposed &&
197
- !this.error &&
191
+ const active = this.nerdFontIcons && !this.disposed && !this.error &&
198
192
  (state === "starting" || state === "running");
199
193
  if (!active) {
200
194
  if (this.progressTimer !== undefined) clearInterval(this.progressTimer);
@@ -339,11 +333,7 @@ export class LiveAgentView {
339
333
  if (this.detail === "compact")
340
334
  return [this.theme.fg("dim", "[Thinking hidden · t cycles view]")];
341
335
  // Thinking is model-authored Markdown; preview truncates the rendered lines, not the source.
342
- const rendered = this.markdown(
343
- String(block.thinking ?? "[redacted thinking]"),
344
- width,
345
- true,
346
- );
336
+ const rendered = this.markdown(String(block.thinking ?? "[redacted thinking]"), width, true);
347
337
  if (this.detail === "full" || rendered.length <= 3) return rendered;
348
338
  return [...rendered.slice(0, 3), this.theme.fg("dim", "...")];
349
339
  }
@@ -459,9 +449,7 @@ export class LiveAgentView {
459
449
  }
460
450
  case "ls": {
461
451
  const limit = num("limit");
462
- lines = [
463
- `${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`,
464
- ];
452
+ lines = [`${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`];
465
453
  break;
466
454
  }
467
455
  default:
@@ -25,7 +25,11 @@ function comparePath(a: string, b: string): number {
25
25
  return a < b ? -1 : a > b ? 1 : 0;
26
26
  }
27
27
 
28
- function chainHas(parentOf: Map<string, string | null>, start: string, target: string): boolean {
28
+ function chainHas(
29
+ parentOf: Map<string, string | null>,
30
+ start: string,
31
+ target: string,
32
+ ): boolean {
29
33
  const seen = new Set<string>();
30
34
  let current: string | null = start;
31
35
  while (current) {
@@ -46,7 +50,8 @@ export function buildStatusTree(
46
50
  ): StatusRow[] {
47
51
  const threadsByPath = new Map<string, ThreadView>();
48
52
  for (const thread of threads) {
49
- if (thread?.path && !threadsByPath.has(thread.path)) threadsByPath.set(thread.path, thread);
53
+ if (thread?.path && !threadsByPath.has(thread.path))
54
+ threadsByPath.set(thread.path, thread);
50
55
  }
51
56
  const nodes = new Map<string, StatusNode>();
52
57
  const parentOf = new Map<string, string | null>();
@@ -61,7 +66,8 @@ export function buildStatusTree(
61
66
  const declaredParent = (path: string): string | null => {
62
67
  if (path === ROOT) return null;
63
68
  const thread = threadsByPath.get(path);
64
- if (thread) return thread.parent && thread.parent !== path ? thread.parent : null;
69
+ if (thread)
70
+ return thread.parent && thread.parent !== path ? thread.parent : null;
65
71
  return lexicalParent(path);
66
72
  };
67
73
  const ensure = (path: string, seen = new Set<string>()): void => {