@gotgenes/pi-subagents 6.11.0 → 6.12.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.
@@ -0,0 +1,45 @@
1
+ ---
2
+ issue: 133
3
+ issue_title: "Inject SDK boundary into `agent-runner`"
4
+ ---
5
+
6
+ # Retro: #133 — Inject SDK boundary into agent-runner
7
+
8
+ ## Final Retrospective (2026-05-22T13:15:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Injected all SDK and IO dependencies into `runAgent()` via a `RunnerIO` interface and `createAgentRunner(io)` factory.
13
+ Eliminated all 14 `vi.mock()` calls across `agent-runner.test.ts` (7) and `agent-runner-extension-tools.test.ts` (7).
14
+ Released as `pi-subagents-v6.11.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The `createAgentRunner(io)` factory pattern was a clean design choice that kept the `AgentRunner` interface and `AgentManager` completely unchanged — zero downstream impact.
21
+ - Folding plan steps 1 and 2 into a single commit was the right call given `tsconfig.json` includes `test/` — recognized the constraint before attempting a broken intermediate commit.
22
+
23
+ #### What caused friction (agent side)
24
+
25
+ 1. `wrong-abstraction` — Annotated `createRunnerIO(): RunnerIO` in the test helper, which erased the `Mock<...>` type information from `vi.fn()` stubs.
26
+ TypeScript then rejected `.mockResolvedValue()` on `io.createSession` across 18 call sites.
27
+ Required removing the annotation plus a follow-up edit to remove the now-unused `type RunnerIO` import flagged by Biome.
28
+ Impact: two extra edit rounds and a type-check cycle before the fix landed.
29
+
30
+ 2. `missing-context` — Added `SettingsManager` to the SDK import block in `index.ts` without checking that the name was already imported from `./settings.js`.
31
+ Biome caught the redeclaration and the `noRedeclare` lint error required an alias fix (`SettingsManager as SdkSettingsManager`).
32
+ Impact: one extra edit round triggered by the autoformat failure.
33
+
34
+ 3. `premature-convergence` — Spent excessive reasoning time deliberating commit-boundary strategy (whether to combine steps 1+2, how to handle broken intermediate states, whether step 4 would have remaining work).
35
+ The answer was straightforward once the `tsconfig` `include` was checked, but the check came late in the deliberation.
36
+ Impact: added friction but no rework — the final decision was correct.
37
+
38
+ #### What caused friction (user side)
39
+
40
+ - None identified.
41
+ The plan and issue were well-specified, and the user's only intervention was "Please, continue" after a message boundary, which was appropriate.
42
+
43
+ ### Changes made
44
+
45
+ 1. `.pi/skills/testing/SKILL.md` — added rule: do not annotate test factory return types with production interface types (erases `Mock<...>` methods).
@@ -0,0 +1,56 @@
1
+ ---
2
+ issue: 134
3
+ issue_title: "Reduce `as any` casts in test suite"
4
+ ---
5
+
6
+ # Retro: #134 — Reduce as-any casts in test suite
7
+
8
+ ## Final Retrospective (2026-05-22T17:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Reduced `as any` casts from 93 to 15 across the pi-subagents package.
13
+ Production changes added type guards (`getToolCallName`, `isBashExecution`), narrowed `SubagentRuntime.widget` to `WidgetLike`, typed `CreateSessionOptions.settingsManager` as `SettingsManager`, and fixed `ResourceLoaderOptions.appendSystemPromptOverride` to match the SDK.
14
+ Test improvements introduced `toAgentSession()`, `STUB_CTX`, and `makeRegistry()` helpers to centralise unavoidable bridge casts.
15
+ Also fixed 3 pre-existing lint issues.
16
+
17
+ This session also included #133 (plan + implement + ship) which preceded the #134 work.
18
+
19
+ ### Observations
20
+
21
+ #### What went well
22
+
23
+ - The user's Kent Beck prompt ("make the change that makes the change easy") redirected the plan from test-only fixes to targeted production changes, yielding cleaner results — `WidgetLike`, type guards, and SDK-typed options eliminated casts that would have been impossible to remove from tests alone.
24
+ - The user's TDA observation on the `MenuCtx` step caught a deep architectural issue (context threaded 4 layers just to relay to `buildParentSnapshot`).
25
+ Skipping step 4 was the right call — attempting it would have added a production cast or cascaded changes through 6+ files.
26
+
27
+ #### What caused friction (agent side)
28
+
29
+ 1. `rabbit-hole` — Step 1 planning involved extensive analysis of whether `CreateSessionOptions` could use full SDK types.
30
+ Spent significant reasoning tracing `ModelRegistry` / `SessionManager` private fields, structural compatibility, `ParentSnapshot` constructibility, and `ResourceLoader` interface width before concluding that only `settingsManager` and the callback signature could be fixed.
31
+ The plan's claim "widen to SDK types" was optimistic about class-with-private-fields constraints.
32
+ Impact: added friction but no rework — the conclusion was correct, just slow to reach.
33
+
34
+ 2. `wrong-abstraction` — Step 4 (`MenuCtx`) attempted to mechanically narrow the handler parameter without questioning why `ctx` was threaded 4 layers deep.
35
+ Partially implemented the `MenuCtx` interface before the user's "What am I misunderstanding?"
36
+ prompt exposed the real issue: a TDA violation where intermediate functions carry `ExtensionContext` only to relay it.
37
+ Impact: partial implementation reverted via `git checkout src/ui/agent-menu.ts`; ~10 minutes of wasted edits.
38
+
39
+ 3. `instruction-violation` (user-caught) — Used `pnpm exec biome check --write --unsafe` instead of `pnpm run lint:fix` to fix pre-existing lint issues.
40
+ The `/tdd-plan` prompt explicitly says "run `pnpm run lint:fix`."
41
+ Impact: no functional difference (same result), but violated the established convention and used an unnecessary `--unsafe` flag.
42
+
43
+ 4. `instruction-violation` (user-caught) — Dismissed pre-existing lint issues as "not from our changes" without fixing them.
44
+ The user asked "Why do we have pre-existing lint issues?"
45
+ — the correct action was to fix them immediately rather than noting and ignoring them.
46
+ Impact: required an extra commit (`style: fix pre-existing lint issues`) that should have been folded into an earlier step.
47
+
48
+ #### What caused friction (user side)
49
+
50
+ - The user's interventions on the `MenuCtx` step and lint issues were both valuable redirections that improved the outcome.
51
+ No mechanical overhead identified — each intervention was strategic judgment that the agent couldn't have reached alone.
52
+
53
+ ### Changes made
54
+
55
+ 1. `.pi/prompts/tdd-plan.md` — added "Verify green baseline" section (check + lint + test before starting TDD); added "Fix all failures — including pre-existing ones" to the end-of-cycle lint step.
56
+ 2. `.pi/prompts/build-plan.md` — same baseline check and fix-all rule for non-TDD plans.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.11.0",
3
+ "version": "6.12.1",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -6,6 +6,7 @@ import type { Model } from "@earendil-works/pi-ai";
6
6
  import {
7
7
  type AgentSession,
8
8
  type AgentSessionEvent,
9
+ type SettingsManager,
9
10
  } from "@earendil-works/pi-coding-agent";
10
11
  import type { AgentConfigLookup } from "./agent-types.js";
11
12
  import { extractText } from "./context.js";
@@ -17,6 +18,23 @@ import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
17
18
  /** Names of tools registered by this extension that subagents must NOT inherit. */
18
19
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
19
20
 
21
+ // ── Local message-shape types ───────────────────────────────────────────────
22
+ // The Pi SDK does not export a narrow type for tool-call content variants.
23
+
24
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn’t export the narrow type. */
25
+ interface ToolCallContent {
26
+ type: "toolCall";
27
+ name?: string;
28
+ toolName?: string;
29
+ }
30
+
31
+ /** Extracts the display name from a tool-call content item. */
32
+ function getToolCallName(c: { type: string }): string {
33
+ if (c.type !== "toolCall") return "unknown";
34
+ const tc = c as ToolCallContent;
35
+ return tc.name ?? tc.toolName ?? "unknown";
36
+ }
37
+
20
38
  /**
21
39
  * Filter the session's active tool names according to extension/denylist rules.
22
40
  *
@@ -82,7 +100,8 @@ export interface ResourceLoaderOptions {
82
100
  noThemes?: boolean;
83
101
  noContextFiles?: boolean;
84
102
  systemPromptOverride?: () => string;
85
- appendSystemPromptOverride?: () => unknown[];
103
+ /** Override the append system prompt. Receives the current base value; return the replacement. */
104
+ appendSystemPromptOverride?: (base: string[]) => string[];
86
105
  }
87
106
 
88
107
  /** Options passed to RunnerIO.createSession. */
@@ -90,7 +109,7 @@ export interface CreateSessionOptions {
90
109
  cwd: string;
91
110
  agentDir: string;
92
111
  sessionManager: SessionManagerLike;
93
- settingsManager: unknown;
112
+ settingsManager: SettingsManager;
94
113
  modelRegistry: unknown;
95
114
  model?: unknown;
96
115
  tools: string[];
@@ -110,7 +129,7 @@ export interface RunnerIO {
110
129
  createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
111
130
  deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
112
131
  createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
113
- createSettingsManager: (cwd: string, agentDir: string) => unknown;
132
+ createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
114
133
  createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
115
134
  assemblerIO: AssemblerIO;
116
135
  }
@@ -446,9 +465,7 @@ export function getAgentConversation(session: AgentSession): string {
446
465
  for (const c of msg.content) {
447
466
  if (c.type === "text" && c.text) textParts.push(c.text);
448
467
  else if (c.type === "toolCall")
449
- toolCalls.push(
450
- ` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`,
451
- );
468
+ toolCalls.push(` Tool: ${getToolCallName(c)}`);
452
469
  }
453
470
  if (textParts.length > 0)
454
471
  parts.push(`[Assistant]: ${textParts.join("\n")}`);
package/src/index.ts CHANGED
@@ -136,7 +136,7 @@ export default function (pi: ExtensionAPI) {
136
136
  const runnerIO: RunnerIO = {
137
137
  detectEnv,
138
138
  getAgentDir,
139
- createResourceLoader: (opts) => new DefaultResourceLoader(opts as any),
139
+ createResourceLoader: (opts) => new DefaultResourceLoader(opts),
140
140
  deriveSessionDir: deriveSubagentSessionDir,
141
141
  createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
142
142
  createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
package/src/renderer.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import type { NotificationDetails } from "./notification.js";
3
- import { formatMs, formatTokens, formatTurns } from "./ui/agent-widget.js";
3
+ import { formatMs, formatTokens, formatTurns } from "./ui/display.js";
4
4
 
5
5
  /** Narrow theme interface — only the methods the renderer actually calls. */
6
6
  interface RendererTheme {
package/src/runtime.ts CHANGED
@@ -7,7 +7,19 @@
7
7
  */
8
8
 
9
9
  import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
10
- import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
10
+ import type { UICtx } from "./ui/agent-widget.js";
11
+
12
+ /**
13
+ * Narrow widget interface consumed by SubagentRuntime delegation methods.
14
+ * AgentWidget satisfies this structurally; tests use plain stubs.
15
+ */
16
+ export interface WidgetLike {
17
+ setUICtx(ctx: UICtx): void;
18
+ onTurnStart(): void;
19
+ markFinished(id: string): void;
20
+ update(): void;
21
+ ensureTimer(): void;
22
+ }
11
23
 
12
24
  /**
13
25
  * Narrow config subset read by AgentManager when constructing RunOptions.
@@ -37,7 +49,7 @@ export class SubagentRuntime {
37
49
  * Persistent widget reference. Null until constructed after AgentManager.
38
50
  * Delegation methods use optional chaining so callers never need `widget!`.
39
51
  */
40
- widget: AgentWidget | null = null;
52
+ widget: WidgetLike | null = null;
41
53
 
42
54
  // ── Session-context methods ──────────────────────────────────────────────
43
55
 
@@ -9,6 +9,7 @@ import { resolveInvocationModel } from "../model-resolver.js";
9
9
 
10
10
  import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
11
11
  import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
12
+ import { type UICtx } from "../ui/agent-widget.js";
12
13
  import {
13
14
  type AgentDetails,
14
15
  buildInvocationTags,
@@ -17,11 +18,10 @@ import {
17
18
  getDisplayName,
18
19
  getPromptModeLabel,
19
20
  SPINNER,
20
- type UICtx,
21
- } from "../ui/agent-widget.js";
21
+ } from "../ui/display.js";
22
22
  import { spawnBackground } from "./background-spawner.js";
23
23
  import { runForeground } from "./foreground-runner.js";
24
- import { buildDetails, buildTypeListText, formatLifetimeTokens, getStatusNote, textResult } from "./helpers.js";
24
+ import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
25
25
 
26
26
  // ---- Deps interface ----
27
27
 
@@ -2,7 +2,7 @@ import type { Model } from "@earendil-works/pi-ai";
2
2
  import type { AgentSpawnConfig } from "../agent-manager.js";
3
3
  import type { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
4
4
  import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
5
- import type { AgentDetails } from "../ui/agent-widget.js";
5
+ import type { AgentDetails } from "../ui/display.js";
6
6
  import { subscribeUIObserver } from "../ui/ui-observer.js";
7
7
  import type { AgentActivityAccess } from "./agent-tool.js";
8
8
  import { textResult } from "./helpers.js";
@@ -8,7 +8,7 @@ import {
8
8
  describeActivity,
9
9
  formatMs,
10
10
  SPINNER,
11
- } from "../ui/agent-widget.js";
11
+ } from "../ui/display.js";
12
12
  import { subscribeUIObserver } from "../ui/ui-observer.js";
13
13
  import type { AgentActivityAccess } from "./agent-tool.js";
14
14
  import {
@@ -2,7 +2,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import type { AgentConfigLookup } from "../agent-types.js";
4
4
  import type { AgentRecord } from "../types.js";
5
- import { formatDuration, getDisplayName } from "../ui/agent-widget.js";
5
+ import { formatDuration, getDisplayName } from "../ui/display.js";
6
6
  import { getSessionContextPercent } from "../usage.js";
7
7
  import { formatLifetimeTokens, textResult } from "./helpers.js";
8
8
 
@@ -1,6 +1,6 @@
1
1
  import type { AgentConfigLookup } from "../agent-types.js";
2
2
  import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
3
- import { type AgentDetails, formatTokens } from "../ui/agent-widget.js";
3
+ import { type AgentDetails, formatTokens } from "../ui/display.js";
4
4
  import { getLifetimeTotal, type LifetimeUsage } from "../usage.js";
5
5
 
6
6
  /** Parenthetical status note for completed agent result text. */
@@ -48,7 +48,7 @@ export function buildDetails(
48
48
 
49
49
  /** Tool execute return value for a text response. */
50
50
  export function textResult(msg: string, details?: unknown) {
51
- return { content: [{ type: "text" as const, text: msg }], details: details as any };
51
+ return { content: [{ type: "text" as const, text: msg }], details };
52
52
  }
53
53
 
54
54
  /** Format an agent's lifetime token total, or "" when zero. */
@@ -10,7 +10,7 @@ import {
10
10
  import type { ModelRegistry } from "../model-resolver.js";
11
11
  import type { AgentConfig, AgentRecord } from "../types.js";
12
12
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
13
- import { formatDuration, getDisplayName } from "./agent-widget.js";
13
+ import { formatDuration, getDisplayName } from "./display.js";
14
14
 
15
15
  // ---- Deps interface ----
16
16
 
@@ -7,40 +7,29 @@
7
7
 
8
8
  import { truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import type { AgentManager } from "../agent-manager.js";
10
- import { type AgentConfigLookup, AgentTypeRegistry } from "../agent-types.js";
11
- import type { AgentInvocation, SubagentType } from "../types.js";
10
+ import { AgentTypeRegistry } from "../agent-types.js";
11
+ import type { SubagentType } from "../types.js";
12
12
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
13
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
14
+ import {
15
+ describeActivity,
16
+ ERROR_STATUSES,
17
+ formatMs,
18
+ formatSessionTokens,
19
+ formatTurns,
20
+ getDisplayName,
21
+ getPromptModeLabel,
22
+ SPINNER,
23
+ type Theme,
24
+ } from "./display.js";
14
25
 
15
26
  // ---- Constants ----
16
27
 
17
28
  /** Maximum number of rendered lines before overflow collapse kicks in. */
18
29
  const MAX_WIDGET_LINES = 12;
19
30
 
20
- /** Braille spinner frames for animated running indicator. */
21
- export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22
-
23
- /** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
24
- export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
25
-
26
- /** Tool name → human-readable action for activity descriptions. */
27
- const TOOL_DISPLAY: Record<string, string> = {
28
- read: "reading",
29
- bash: "running command",
30
- edit: "editing",
31
- write: "writing",
32
- grep: "searching",
33
- find: "finding files",
34
- ls: "listing",
35
- };
36
-
37
31
  // ---- Types ----
38
32
 
39
- export type Theme = {
40
- fg(color: string, text: string): string;
41
- bold(text: string): string;
42
- };
43
-
44
33
  export type UICtx = {
45
34
  setStatus(key: string, text: string | undefined): void;
46
35
  setWidget(
@@ -50,147 +39,6 @@ export type UICtx = {
50
39
  ): void;
51
40
  };
52
41
 
53
- /** Metadata attached to Agent tool results for custom rendering. */
54
- export interface AgentDetails {
55
- displayName: string;
56
- description: string;
57
- subagentType: string;
58
- toolUses: number;
59
- tokens: string;
60
- durationMs: number;
61
- status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
62
- /** Human-readable description of what the agent is currently doing. */
63
- activity?: string;
64
- /** Current spinner frame index (for animated running indicator). */
65
- spinnerFrame?: number;
66
- /** Short model name if different from parent (e.g. "haiku", "sonnet"). */
67
- modelName?: string;
68
- /** Notable config tags (e.g. ["thinking: high", "isolated"]). */
69
- tags?: string[];
70
- /** Current turn count. */
71
- turnCount?: number;
72
- /** Effective max turns (undefined = unlimited). */
73
- maxTurns?: number;
74
- agentId?: string;
75
- error?: string;
76
- }
77
-
78
- // ---- Formatting helpers ----
79
-
80
- /** Format a token count compactly: "33.8k token", "1.2M token". */
81
- export function formatTokens(count: number): string {
82
- if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
83
- if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
84
- return `${count} token`;
85
- }
86
-
87
- /**
88
- * Token count with optional context-fill % and compaction-count annotations.
89
- * Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
90
- * Compaction count rendered as `↻N` in dim.
91
- *
92
- * "12.3k token" — no annotations
93
- * "12.3k token (45%)" — percent only
94
- * "12.3k token (↻2)" — compactions only (e.g. right after compact)
95
- * "12.3k token (45% · ↻2)" — both
96
- */
97
- export function formatSessionTokens(
98
- tokens: number,
99
- percent: number | null,
100
- theme: Theme,
101
- compactions = 0,
102
- ): string {
103
- const tokenStr = formatTokens(tokens);
104
- const annot: string[] = [];
105
- if (percent !== null) {
106
- const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
107
- annot.push(theme.fg(color, `${Math.round(percent)}%`));
108
- }
109
- if (compactions > 0) {
110
- annot.push(theme.fg("dim", `↻${compactions}`));
111
- }
112
- if (annot.length === 0) return tokenStr;
113
- return `${tokenStr} (${annot.join(" · ")})`;
114
- }
115
-
116
- /** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
117
- export function formatTurns(turnCount: number, maxTurns?: number | null): string {
118
- return maxTurns != null ? `⟳${turnCount}≤${maxTurns}` : `⟳${turnCount}`;
119
- }
120
-
121
- /** Format milliseconds as human-readable duration. */
122
- export function formatMs(ms: number): string {
123
- return `${(ms / 1000).toFixed(1)}s`;
124
- }
125
-
126
- /** Format duration from start/completed timestamps. */
127
- export function formatDuration(startedAt: number, completedAt?: number): string {
128
- if (completedAt) return formatMs(completedAt - startedAt);
129
- return `${formatMs(Date.now() - startedAt)} (running)`;
130
- }
131
-
132
- /** Get display name for any agent type (built-in or custom). */
133
- export function getDisplayName(type: SubagentType, registry: AgentConfigLookup): string {
134
- const config = registry.resolveAgentConfig(type);
135
- return config.displayName ?? config.name;
136
- }
137
-
138
- /** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
139
- export function getPromptModeLabel(type: SubagentType, registry: AgentConfigLookup): string | undefined {
140
- const config = registry.resolveAgentConfig(type);
141
- return config.promptMode === "append" ? "twin" : undefined;
142
- }
143
-
144
- /** Mode label is not included — callers add it where they want it. */
145
- export function buildInvocationTags(
146
- invocation: AgentInvocation | undefined,
147
- ): { modelName?: string; tags: string[] } {
148
- const tags: string[] = [];
149
- if (!invocation) return { tags };
150
- if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
151
- if (invocation.isolated) tags.push("isolated");
152
- if (invocation.isolation === "worktree") tags.push("worktree");
153
- if (invocation.inheritContext) tags.push("inherit context");
154
- if (invocation.runInBackground) tags.push("background");
155
- if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
156
- return { modelName: invocation.modelName, tags };
157
- }
158
-
159
- /** Truncate text to a single line, max `len` chars. */
160
- function truncateLine(text: string, len = 60): string {
161
- const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
162
- if (line.length <= len) return line;
163
- return line.slice(0, len) + "…";
164
- }
165
-
166
- /** Build a human-readable activity string from currently-running tools or response text. */
167
- export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
168
- if (activeTools.size > 0) {
169
- const groups = new Map<string, number>();
170
- for (const toolName of activeTools.values()) {
171
- const action = TOOL_DISPLAY[toolName] ?? toolName;
172
- groups.set(action, (groups.get(action) ?? 0) + 1);
173
- }
174
-
175
- const parts: string[] = [];
176
- for (const [action, count] of groups) {
177
- if (count > 1) {
178
- parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
179
- } else {
180
- parts.push(action);
181
- }
182
- }
183
- return parts.join(", ") + "…";
184
- }
185
-
186
- // No tools active — show truncated response text if available
187
- if (responseText && responseText.trim().length > 0) {
188
- return truncateLine(responseText);
189
- }
190
-
191
- return "thinking…";
192
- }
193
-
194
42
  // ---- Widget manager ----
195
43
 
196
44
  export class AgentWidget {
@@ -12,7 +12,38 @@ import { extractText } from "../context.js";
12
12
  import type { AgentRecord } from "../types.js";
13
13
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
14
14
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
15
- import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
15
+ import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./display.js";
16
+
17
+ // ── Local message-shape types ───────────────────────────────────────────────
18
+ // The Pi SDK does not export narrow types for all message content variants.
19
+ // These file-local types document the runtime shapes this module handles.
20
+
21
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn't export the narrow type. */
22
+ interface ToolCallContent {
23
+ type: "toolCall";
24
+ name?: string;
25
+ toolName?: string;
26
+ }
27
+
28
+ /** Extracts the tool name from a content item, falling back to 'unknown'. */
29
+ function getToolCallName(c: { type: string }): string {
30
+ if (c.type !== "toolCall") return "unknown";
31
+ const tc = c as ToolCallContent;
32
+ return tc.name ?? tc.toolName ?? "unknown";
33
+ }
34
+
35
+ /** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
36
+ interface BashExecutionMessage {
37
+ role: "bashExecution";
38
+ command: string;
39
+ output?: string;
40
+ }
41
+
42
+ function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
43
+ return msg.role === "bashExecution";
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
16
47
 
17
48
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
18
49
  const CHROME_LINES_BASE = 6;
@@ -228,7 +259,7 @@ export class ConversationViewer implements Component {
228
259
  for (const c of msg.content) {
229
260
  if (c.type === "text" && c.text) textParts.push(c.text);
230
261
  else if (c.type === "toolCall") {
231
- toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
262
+ toolCalls.push(getToolCallName(c));
232
263
  }
233
264
  }
234
265
  if (needsSeparator) lines.push(th.fg("dim", "───"));
@@ -250,14 +281,13 @@ export class ConversationViewer implements Component {
250
281
  for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
251
282
  lines.push(th.fg("dim", line));
252
283
  }
253
- } else if ((msg as any).role === "bashExecution") {
254
- const bash = msg as any;
284
+ } else if (isBashExecution(msg)) {
255
285
  if (needsSeparator) lines.push(th.fg("dim", "───"));
256
- lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
257
- if (bash.output?.trim()) {
258
- const out = bash.output.length > 500
259
- ? bash.output.slice(0, 500) + "... (truncated)"
260
- : bash.output;
286
+ lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
287
+ if (msg.output?.trim()) {
288
+ const out = msg.output.length > 500
289
+ ? msg.output.slice(0, 500) + "... (truncated)"
290
+ : msg.output;
261
291
  for (const line of wrapTextWithAnsi(out.trim(), width)) {
262
292
  lines.push(th.fg("dim", line));
263
293
  }