pi-claude-agent-sdk 0.7.1 → 0.8.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.
package/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/pi-claude-agent-sdk)](https://www.npmjs.com/package/pi-claude-agent-sdk)
4
4
 
5
- Pi extension that integrates Claude Code via the [Agent SDK](https://github.com/anthropics/claude-agent-sdk-typescript). Forked from [pi-claude-bridge](https://github.com/elidickinson/pi-claude-bridge) by Eli Dickinson, which was based initially on [claude-agent-sdk-pi](https://github.com/prateekmedia/claude-agent-sdk-pi) by Prateek Sunal. Adds streaming, MCP tool bridging, custom pi tool bridging, session resume/persistence, context sync, thinking support, skills forwarding, and the AskClaude tool.
6
-
7
- 1. **Provider** — Use Opus/Sonnet/Haiku as models in pi, with all tool calls flowing through pi's TUI
8
- 2. **AskClaude tool** — Delegate tasks or questions to Claude Code when using another provider
5
+ Pi extension that integrates Claude Code as a pi model provider via the [Agent SDK](https://github.com/anthropics/claude-agent-sdk-typescript). Forked from [pi-claude-bridge](https://github.com/elidickinson/pi-claude-bridge) by Eli Dickinson, which was based initially on [claude-agent-sdk-pi](https://github.com/prateekmedia/claude-agent-sdk-pi) by Prateek Sunal. Adds streaming, MCP tool bridging, custom pi tool bridging, session resume/persistence, context sync, thinking support, and skills forwarding.
9
6
 
7
+ Use Opus/Sonnet/Haiku as models in pi, with all tool calls flowing through pi's TUI.
10
8
 
11
9
  **FYI:** Anthropic [announced and then unannounced](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) a change to how you would be billed for tools that use the Agent SDK like this one. As of June 15, 2026 it uses subscription quota just like Claude Code direct does.
12
10
 
@@ -27,42 +25,16 @@ Use `/model` to select `claude-bridge/claude-fable-5`, `claude-bridge/claude-opu
27
25
 
28
26
  Behind the scenes, pi's tools are bridged to Claude Code but it should all work like normal in pi. Bash commands get a 120-second default timeout (matching Claude Code's default) since pi's bash has no timeout by default. Skills in pi are copied over to Claude Code's system prompt so should work as they would with any other pi provider. Steering works mid-turn: a message sent while Claude is running a tool reaches it at that tool boundary, not after the whole turn finishes.
29
27
 
30
- **Authentication:** the bridge requires an Anthropic OAuth credential (or API key) configured in Pi and uses Pi's normal token refresh. Claude Code login and inherited Claude/Anthropic authentication settings are deliberately ignored, so configure Anthropic authentication in Pi before using the provider or AskClaude.
28
+ **Authentication:** the bridge requires an Anthropic OAuth credential (or API key) configured in Pi and uses Pi's normal token refresh. Claude Code login and inherited Claude/Anthropic authentication settings are deliberately ignored, so configure Anthropic authentication in Pi before using the provider.
31
29
 
32
30
  **1M Context:** Opus 5, Opus 4.8, and Opus 4.7 get 1M context by default. Opus 4.6 only gets 1M if you're on a Max plan or pay for Extra Usage. Sonnet 4.6 only gets 1M if you pay for Extra Usage. You will need to set `provider.plan` and/or `provider.longContextExtraUsage` for 1M context in Opus 4.6/Sonnet 4.6 as described in [Configuration](#configuration).
33
31
 
34
- ## AskClaude Tool
35
-
36
- Available when using any non-claude-bridge provider. Pi's LLM can delegate tasks to Claude Code and wait for it to answer a question or perform a task. Examples of how to use:
37
-
38
- - "Ask Claude to plan a fix"
39
- - "If you get stuck, ask claude for help"
40
- - "Ask claude to review the plan in @foo.md, implement it, then ask an isolated=true claude to review the implementation"
41
- - "Ask claude to poke holes in this theory"
42
- - "Find all the places in the codebase that handle auth"
43
-
44
- You could also create skills or add something to AGENTS.md to e.g. "Always call Ask Claude to review complicated feature implementations before considering the task complete."
45
-
46
- ### Parameters
47
-
48
- - **`prompt`** — the question or task for Claude Code
49
- - **`mode`** — `read` (default, read files and search/fetch on web), `none`, or `full` (read+write+bash, disable this mode with `allowFullMode: false` in config)
50
- - **`model`** — `opus` (default), `sonnet`, `haiku`, or a full model ID
51
- - **`thinking`** — effort level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`
52
- - **`isolated`** — when `true`, Claude gets a clean session with no conversation history (default: `false`)
53
-
54
32
  ## Configuration
55
33
 
56
34
  Config: `~/.pi/agent/claude-bridge.json` (global) or the project Pi config directory, usually `.pi/claude-bridge.json` (project; merged over global).
57
35
 
58
36
  ```json
59
37
  {
60
- "askClaude": {
61
- "enabled": true,
62
- "allowFullMode": true,
63
- "defaultIsolated": false,
64
- "description": "Custom tool description override"
65
- },
66
38
  "provider": {
67
39
  "plan": "max",
68
40
  "longContextExtraUsage": false,
@@ -72,18 +44,8 @@ Config: `~/.pi/agent/claude-bridge.json` (global) or the project Pi config direc
72
44
  }
73
45
  ```
74
46
 
75
- `askClaude`:
76
- - `enabled` — register the AskClaude tool (default `true`)
77
- - `name` — override the tool's pi-side name (default `"AskClaude"`)
78
- - `label` — override the TUI label (default `"Ask Claude Code"`)
79
- - `description` — override the tool description. Default when `allowFullMode: true`: *"Delegate to Claude Code for a second opinion or analysis (code review, architecture questions, debugging theories), or to autonomously handle a task. Defaults to read-only mode — use full mode when the user wants to delegate a task that requires changes. Prefer to handle straightforward tasks yourself."*
80
- - `defaultMode` — `"read"` (default), `"none"`, or `"full"`
81
- - `defaultIsolated` — start each call in a fresh session (default `false`)
82
- - `allowFullMode` — allow `mode: "full"`; set `false` to lock it out
83
- - `appendSkills` — forward pi's skills block into the system prompt (default `true`)
84
-
85
47
  `provider`:
86
- - `plan` (default `"pro"`) — set to `"max"` for Max (or Team Premium/Enterprise) to enable Opus 4.6 with 1M context. If it's unset, the first interactive session points this out once, then records `startupNoticeShown` (the date, `YYYY-MM-DD`) in the global config so it doesn't nag again.
48
+ - `plan` (default `"max"`) — Max (or Team Premium/Enterprise). Set to `"pro"` on a Pro plan so Opus 4.6 stays at 200K context. If it's unset, the first interactive session points this out once, then records `startupNoticeShown` (the date, `YYYY-MM-DD`) in the global config so it doesn't nag again.
87
49
  - `longContextExtraUsage` — set to `true` to enable 1M models that cost money through Extra Usage. It enables Sonnet 4.6 with 1M on every plan and Opus 4.6 with 1M on Pro. Not needed for Opus 4.7 or 4.8.
88
50
  - `appendSystemPrompt` — append pi's project context files (global and ancestor `AGENTS.md` / `CLAUDE.md`) and skills (default `true`)
89
51
  - `settingSources` — CC filesystem settings to load; only applied when `appendSystemPrompt: false`
@@ -91,14 +53,13 @@ Config: `~/.pi/agent/claude-bridge.json` (global) or the project Pi config direc
91
53
  - `autoMemoryEnabled` — enable Claude Code's auto-memory system (default `false`)
92
54
  - `pathToClaudeCodeExecutable` — path to the `claude` binary. Useful if your OS/filesystem has the SDK's bundled musl/glibc binaries in a place where they can't run. For example, with Nix you can set the binary to e.g. `"/home/you/.nix-profile/bin/claude"`.
93
55
 
94
-
95
56
  **Extension providers and models.json:** pi's `modelOverrides` in `~/.pi/agent/models.json` do not currently apply to extension-registered providers (like claude-bridge). Overriding `contextWindow` or other fields requires editing `src/models.ts` directly.
96
57
 
97
58
  ## Tests
98
59
 
99
- `npm run test:unit` for offline tests (`tests/unit-*.mjs`: queue, import, skills).
60
+ `npm run test:unit` for offline tests (`tests/unit-*.mjs`: queue, import, skills).
100
61
 
101
- `npm test` for the full suite, which adds integration tests that hit APIs (`tests/int-*.{sh,mjs}`: smoke, multi-turn, cache, session-resume, session-rebuild, tool-message). Set `CLAUDE_BRIDGE_TESTING_ALT_MODEL` in `.env.test` for the alt-provider smoke test (e.g. `openrouter/z-ai/glm-4.7-flash`).
62
+ `npm test` for the full suite, which adds integration tests that hit APIs (`tests/int-*.{sh,mjs}`: smoke, multi-turn, cache, session-resume, session-rebuild, tool-message). Set `CLAUDE_BRIDGE_TESTING_ALT_PROVIDER` and `CLAUDE_BRIDGE_TESTING_ALT_MODEL` in `.env.test` for the provider-switch tests.
102
63
 
103
64
  Integration tests spawn real `pi` and Claude Code subprocesses, so they need write access to `~/.claude` for CC's session state — a sandbox that blocks it makes the next turn's `--resume` fail with `No conversation found with session ID`. The RPC harness probes for this at startup and fails fast.
104
65
 
@@ -107,10 +68,6 @@ Integration tests spawn real `pi` and Claude Code subprocesses, so they need wri
107
68
  Set `CLAUDE_BRIDGE_DEBUG=1` to enable debug output:
108
69
 
109
70
  - **Bridge log** at `~/.pi/agent/claude-bridge.log` — every provider call, session sync decision, tool result delivery, and CC's stderr. Override location with `CLAUDE_BRIDGE_DEBUG_PATH`.
110
- - **Per-query Claude Code CLI logs** at `~/.pi/agent/cc-cli-logs/<timestamp>-<tag>-<seq>.log` — the CC subprocess's own debug stream, one file per `query()` call. Tags are `provider` (main turn) or `askclaude` (sub-delegation). Useful when a resume fails or CC misbehaves internally — shows the CLI's own view of session loading, API requests, and tool calls.
71
+ - **Per-query Claude Code CLI logs** at `~/.pi/agent/cc-cli-logs/<timestamp>-<tag>-<seq>.log` — the CC subprocess's own debug stream, one file per `query()` call. Tags are `provider` (main turn) or `compact-summary`. Useful when a resume fails or CC misbehaves internally — shows the CLI's own view of session loading, API requests, and tool calls.
111
72
 
112
73
  When filing a bug about a session-resume failure (e.g. "No conversation found"), the most useful attachments are the `syncResult:` lines from the bridge log plus the matching `cc-cli-logs/` file for the failing query.
113
-
114
- ## Maintenance
115
-
116
- After a Claude Code release, review `MODE_DISALLOWED_TOOLS` in `src/index.ts` — it gates which CC tools the AskClaude subagent may invoke per mode (`read` / `full` / `none`). Add new agentic tools (PlanMode, Task spawning, etc.) to the appropriate mode lists if they shouldn't be available to subagents.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-claude-agent-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
- "description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider and adds an AskClaude tool.",
5
+ "description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider.",
6
6
  "keywords": [
7
7
  "pi-package",
8
8
  "pi",
@@ -44,20 +44,18 @@
44
44
  "@anthropic-ai/sdk": "^0.73.0",
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
46
  "cc-session-io": "^0.3.2",
47
- "change-case": "^5.4.4",
48
- "typebox": "^1.3.0"
47
+ "change-case": "^5.4.4"
49
48
  },
50
49
  "peerDependencies": {
51
50
  "@earendil-works/pi-ai": ">=0.82.1",
52
- "@earendil-works/pi-coding-agent": ">=0.82.1",
53
- "@earendil-works/pi-tui": ">=0.82.1"
51
+ "@earendil-works/pi-coding-agent": ">=0.82.1"
54
52
  },
55
53
  "devDependencies": {
56
54
  "@earendil-works/pi-ai": "^0.82.1",
57
55
  "@earendil-works/pi-coding-agent": "^0.82.1",
58
- "@earendil-works/pi-tui": "^0.82.1",
59
56
  "@types/node": "^24.13.2",
60
57
  "tsx": "^4.22.4",
58
+ "typebox": "^1.3.0",
61
59
  "typescript": "^6.0.3"
62
60
  },
63
61
  "pi": {
package/src/child-env.ts CHANGED
@@ -46,7 +46,7 @@ export function buildClaudeChildEnv(
46
46
  const headerApiKey = headers.find(([name]) => name.toLowerCase() === "x-api-key")?.[1];
47
47
  const credential = resolved?.auth.apiKey ?? headerApiKey;
48
48
  if (!credential && !bearerToken) {
49
- throw new Error("No Anthropic credential is configured in Pi. Configure Anthropic authentication in Pi before using claude-bridge or AskClaude.");
49
+ throw new Error("No Anthropic credential is configured in Pi. Configure Anthropic authentication in Pi before using claude-bridge.");
50
50
  }
51
51
 
52
52
  if (resolved?.auth.baseUrl) env.ANTHROPIC_BASE_URL = resolved.auth.baseUrl;
package/src/config.ts CHANGED
@@ -12,16 +12,6 @@ import { dirname, join } from "path";
12
12
  export interface Config {
13
13
  /** Date (YYYY-MM-DD) the one-time startup notice was shown. Written by the extension, not the user. */
14
14
  startupNoticeShown?: string;
15
- askClaude?: {
16
- enabled?: boolean;
17
- name?: string;
18
- label?: string;
19
- description?: string;
20
- defaultMode?: "full" | "read" | "none";
21
- defaultIsolated?: boolean;
22
- allowFullMode?: boolean;
23
- appendSkills?: boolean;
24
- };
25
15
  /** Low-level Claude Agent SDK plumbing. Most users won't need these. */
26
16
  provider?: {
27
17
  appendSystemPrompt?: boolean;
@@ -29,7 +19,7 @@ export interface Config {
29
19
  strictMcpConfig?: boolean;
30
20
  autoMemoryEnabled?: boolean;
31
21
  pathToClaudeCodeExecutable?: string;
32
- // Subscription plan tier. Setting to "max" enables Opus 4.6 at 1M context
22
+ // Subscription plan tier. Defaults to "max". Set to "pro" to keep Opus 4.6 at 200K.
33
23
  plan?: "pro" | "max";
34
24
  // Set to true to opt into metered 1M context usage ("extra usage" in
35
25
  // Anthropic billing). Enables Sonnet 4.6 [1m] on every plan and Opus 4.6
@@ -72,7 +62,6 @@ export function loadConfig(cwd: string): Config {
72
62
  const project = tryParseJson(join(cwd, CONFIG_DIR_NAME, "claude-bridge.json"));
73
63
  return {
74
64
  startupNoticeShown: project.startupNoticeShown ?? global.startupNoticeShown,
75
- askClaude: { ...global.askClaude, ...project.askClaude },
76
65
  provider: { ...global.provider, ...project.provider },
77
66
  };
78
67
  }
package/src/convert.ts CHANGED
@@ -9,8 +9,8 @@ import { MCP_TOOL_PREFIX } from "./skills.js";
9
9
 
10
10
  export const PROVIDER_ID = "claude-bridge";
11
11
 
12
- // Pi tool names under Claude Code's builtin names. Only ever correct on the
13
- // AskClaude path, where CC runs its own tools — see mapPiToolNameToSdk.
12
+ // Pi tool names under Claude Code's builtin names. Used when conversion runs
13
+ // without the provider's MCP tool map.
14
14
  export const PI_TO_SDK_TOOL_NAME: Record<string, string> = {
15
15
  read: "Read", write: "Write", edit: "Edit", bash: "Bash",
16
16
  };
@@ -49,14 +49,14 @@ export function sanitizeToolId(id: string, cache: Map<string, string>): string {
49
49
  * tool Claude can call is a pi tool served over MCP, and its name is
50
50
  * `mcp__custom-tools__<pi name>` by construction (resolveMcpTools). The map
51
51
  * is consulted first only because it carries the served tool's exact casing.
52
- * A name it lacks is a tool pi ran that we do not serve now — AskClaude,
53
- * excluded on purpose, or an extension since disabled — and naming that after
54
- * a Claude Code builtin would tell the model a builtin it cannot call is
55
- * available and was already used. That is the prompt condition behind the
56
- * phantom-call deadlock fixed in 122914dd, and the read direction refuses the
57
- * same names for the same reason (piToolNameFor in index.ts).
58
- * - **Without a map — the AskClaude path.** CC runs its own tools there, so
59
- * builtin names are real, matching mapToolName in the other direction.
52
+ * A name it lacks is a tool pi ran that we do not serve now — for example,
53
+ * an extension since disabled — and naming that after a Claude Code builtin
54
+ * would tell the model a builtin it cannot call is available and was already
55
+ * used. That is the prompt condition behind the phantom-call deadlock fixed
56
+ * in 122914dd, and the read direction refuses the same names for the same
57
+ * reason (piToolNameFor in index.ts).
58
+ * - **Without a map.** Preserve the legacy direct-conversion behavior, where pi's
59
+ * standard tool names map to Claude Code builtins.
60
60
  */
61
61
  export function mapPiToolNameToSdk(name: string, customToolNameToSdk?: Map<string, string>): string {
62
62
  if (!name) return "";
package/src/index.ts CHANGED
@@ -1,17 +1,15 @@
1
- import { calculateCost, StringEnum, type AssistantMessage, type AssistantMessageEventStream, type Context, type ImageContent, type Model, type SimpleStreamOptions, type TextContent, type Tool, type UserMessage } from "@earendil-works/pi-ai";
1
+ import { calculateCost, type AssistantMessage, type AssistantMessageEventStream, type Context, type ImageContent, type Model, type SimpleStreamOptions, type TextContent, type Tool, type UserMessage } from "@earendil-works/pi-ai";
2
2
  import * as piAi from "@earendil-works/pi-ai";
3
3
  import { getModels } from "@earendil-works/pi-ai/compat";
4
- import { buildSessionContext, compact, keyHint, type CompactionEntry, type ExtensionAPI, type ExtensionContext, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
4
+ import { compact, type CompactionEntry, type ExtensionAPI, type ExtensionContext, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
5
5
  import { query, type EffortLevel, type SDKMessage, type SettingSource } from "@anthropic-ai/claude-agent-sdk";
6
6
  import type { Base64ImageSource, ContentBlockParam } from "@anthropic-ai/sdk/resources";
7
- import { Type } from "typebox";
8
- import { Text } from "@earendil-works/pi-tui";
9
7
  import { createSession, deleteSession, repairToolPairing } from "cc-session-io";
10
8
  import { appendFileSync, mkdirSync, realpathSync, statSync } from "fs";
11
9
  import { homedir } from "os";
12
10
  import { dirname, join } from "path";
13
11
  import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
14
- import { applyLongContext, buildModels, claudeCodeModelId, type LongContextSettings, resolveModel as _resolveModel } from "./models.js";
12
+ import { applyLongContext, buildModels, claudeCodeModelId, type LongContextSettings } from "./models.js";
15
13
  import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, extractSkillsBlock } from "./skills.js";
16
14
  import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
17
15
  import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
@@ -20,7 +18,6 @@ import { makePromptStream, userMessage, type PromptStream } from "./prompt-strea
20
18
  import { claudeCodeSettings, loadConfig, markStartupNoticeShown, type Config } from "./config.js";
21
19
  import { extractAgentsAppend } from "./agents-md.js";
22
20
  import { createToolServer } from "./mcp-server.js";
23
- import { buildActionSummary, type ToolCallState } from "./askclaude-ui.js";
24
21
  import { CC_CHILD_ENV, resolveClaudeChildEnv, type AnthropicAuthRegistry } from "./child-env.js";
25
22
 
26
23
  // Compat (#2): use factory if available (pi-ai ≥0.66), else fall back to constructor (gsd-pi etc.)
@@ -121,20 +118,10 @@ function diagDump(label: string, data: Record<string, unknown>) {
121
118
  // registration can occur for the next session.
122
119
  const ACTIVE_STREAM_SIMPLE_KEY = Symbol.for("claude-bridge:activeStreamSimple");
123
120
 
124
- // Claude Code's own builtin tools, for the AskClaude path where CC really runs
125
- // them. The provider path never sees these — it starts CC with `tools: []`.
126
- const SDK_TO_PI_TOOL_NAME: Record<string, string> = {
127
- read: "read", write: "write", edit: "edit", bash: "bash",
128
- };
129
-
130
121
  // MODELS is buildModels(getModels("anthropic")) — projection kept in models.js.
131
122
  const MODELS = buildModels(getModels("anthropic"));
132
123
  let providerSettings: NonNullable<Config["provider"]> = {};
133
- let longContextSettings: LongContextSettings = { plan: "pro", longContextExtraUsage: false };
134
-
135
- function resolveModel(input: string) {
136
- return _resolveModel(MODELS, input);
137
- }
124
+ let longContextSettings: LongContextSettings = { plan: "max", longContextExtraUsage: false };
138
125
 
139
126
  // --- Error handling ---
140
127
 
@@ -149,32 +136,6 @@ function errorMessage(err: unknown): string {
149
136
  return String(err);
150
137
  }
151
138
 
152
- // AskClaude mode presets — controls which CC tools are blocked per mode.
153
- // Only block tools that can't work (no pi TUI for user interaction).
154
- // Other CC tools (Agent, SendMessage, RemoteTrigger, Tasks, etc.) are intentionally not blocked.
155
- const ASKCLAUDE_ALWAYS_BLOCKED = [
156
- "AskUserQuestion", "EnterPlanMode", "ExitPlanMode",
157
- "ToolSearch", // probes for blocked tools, wastes tokens
158
- "ScheduleWakeup", // no harness to fire wakeup from inside a delegated subagent
159
- ];
160
- const MODE_DISALLOWED_TOOLS: Record<string, string[]> = {
161
- full: [
162
- ...ASKCLAUDE_ALWAYS_BLOCKED,
163
- ],
164
- read: [
165
- ...ASKCLAUDE_ALWAYS_BLOCKED,
166
- "Write", "Edit", "Bash", "NotebookEdit",
167
- "EnterWorktree", "ExitWorktree", "CronCreate", "CronDelete", "TeamCreate", "TeamDelete",
168
- ],
169
- none: [
170
- ...ASKCLAUDE_ALWAYS_BLOCKED,
171
- "Read", "Write", "Edit", "Glob", "Grep", "Bash", "Agent",
172
- "NotebookEdit", "EnterWorktree", "ExitWorktree",
173
- "CronCreate", "CronDelete", "TeamCreate", "TeamDelete",
174
- "WebFetch", "WebSearch",
175
- ],
176
- };
177
-
178
139
  // --- Session persistence ---
179
140
 
180
141
  interface SessionState {
@@ -588,8 +549,7 @@ function syncSharedSession(
588
549
  // cursor, so it lands here, gets a fresh session, and the ephemeral session it
589
550
  // captures is deleted once its query completes (see preserveSharedSession in
590
551
  // the completion handler). Remove this branch and a subagent resumes — then
591
- // overwrites — the parent's session. The non-isolated AskClaude path reaches it
592
- // the same way.
552
+ // overwrites — the parent's session.
593
553
  //
594
554
  // It is NOT, despite an earlier comment here, the isolated compact-summary
595
555
  // path: runIsolatedSummary never calls syncSharedSession at all.
@@ -667,15 +627,6 @@ export const __test = {
667
627
 
668
628
  // --- Provider helpers: tool name mapping ---
669
629
 
670
- // AskClaude path: CC runs its own tools, so builtin names are real.
671
- function mapToolName(name: string): string {
672
- const normalized = name.toLowerCase();
673
- const builtin = SDK_TO_PI_TOOL_NAME[normalized];
674
- if (builtin) return builtin;
675
- if (normalized.startsWith(MCP_TOOL_PREFIX)) return name.slice(MCP_TOOL_PREFIX.length);
676
- return name;
677
- }
678
-
679
630
  // Provider path: the query runs with `tools: []`, so the only tools CC can
680
631
  // legitimately call are the pi tools we serve over MCP. Any other name is the
681
632
  // model hallucinating a builtin (`bash`, `Bash`, `Edit`, an MCP server we don't
@@ -726,10 +677,10 @@ let piMode: ExtensionContext["mode"] | null = null;
726
677
  let piModelRegistry: AnthropicAuthRegistry | null = null;
727
678
  const activeQueryContexts = new Set<QueryContext>();
728
679
 
729
- // `plan` is the one setting whose default silently costs the user something (no
730
- // Opus 1M on Max), so announce it once. Deferred to the first bridge query
731
- // rather than session_start: the notice persists a flag to the global config,
732
- // and firing it on startup would write that file for every pi session that
680
+ // `plan` is the one setting whose default can cost a Pro user something (Opus
681
+ // 4.6 at 1M when they only have 200K), so announce it once. Deferred to the first
682
+ // provider query rather than session_start: the notice persists a flag to the global
683
+ // config, and firing it on startup would write that file for every pi session that
733
684
  // merely has this extension installed.
734
685
  let planNoticePending = false;
735
686
 
@@ -740,7 +691,7 @@ function showPlanNoticeOnce(): void {
740
691
  planNoticePending = false;
741
692
  const path = markStartupNoticeShown();
742
693
  piUI?.notify(
743
- `Claude bridge: assuming a Pro plan. On Max (or Team Premium/Enterprise), set provider.plan to "max" in ${path} to unlock Opus at 1M context.`,
694
+ `pi-claude-agent-sdk: assuming a Max plan. On Pro, set provider.plan to "pro" in ${path} so Opus 4.6 stays at 200K context.`,
744
695
  "info",
745
696
  );
746
697
  }
@@ -765,7 +716,7 @@ function contextForToolResults(results: McpResult[]): QueryContext | undefined {
765
716
  return undefined;
766
717
  }
767
718
 
768
- function resolveMcpTools(context: Context, excludeToolName?: string): {
719
+ function resolveMcpTools(context: Context): {
769
720
  mcpTools: Tool[];
770
721
  customToolNameToSdk: Map<string, string>;
771
722
  customToolNameToPi: Map<string, string>;
@@ -777,7 +728,6 @@ function resolveMcpTools(context: Context, excludeToolName?: string): {
777
728
  if (!context.tools) return { mcpTools, customToolNameToSdk, customToolNameToPi };
778
729
 
779
730
  for (const tool of context.tools) {
780
- if (tool.name === excludeToolName) continue;
781
731
  const sdkName = `${MCP_TOOL_PREFIX}${tool.name}`;
782
732
  mcpTools.push(tool);
783
733
  customToolNameToSdk.set(tool.name, sdkName);
@@ -1285,7 +1235,7 @@ async function deliverToolResults(
1285
1235
  }
1286
1236
  if (c.pendingToolCalls.size > 0) {
1287
1237
  debug(`WARNING: ${c.pendingToolCalls.size} MCP handlers still waiting after delivering ${results.length} results`);
1288
- piUI?.notify(`Claude bridge: ${c.pendingToolCalls.size} tool handler(s) still waiting — provider may be stuck`, "warning");
1238
+ piUI?.notify(`pi-claude-agent-sdk: ${c.pendingToolCalls.size} tool handler(s) still waiting — provider may be stuck`, "warning");
1289
1239
  }
1290
1240
  }
1291
1241
 
@@ -1381,7 +1331,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1381
1331
  queryCtx.resetTurnState(model);
1382
1332
  queryCtx.latestCursor = 0;
1383
1333
 
1384
- const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context, askClaudeToolName);
1334
+ const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context);
1385
1335
  const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
1386
1336
  // cliModel is the actual id sent to Claude Code (may carry [1m]); model.id is the
1387
1337
  // pi-registered id. Log cliModel so debug lines reflect what CC actually received.
@@ -1604,179 +1554,9 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
1604
1554
  return stream;
1605
1555
  }
1606
1556
 
1607
- // --- AskClaude: prompt and wait ---
1608
-
1609
- async function promptAndWait(
1610
- prompt: string,
1611
- mode: "full" | "read" | "none",
1612
- toolCalls: Map<string, ToolCallState>,
1613
- signal?: AbortSignal,
1614
- options?: {
1615
- systemPrompt?: string;
1616
- appendSkills?: boolean;
1617
- onStreamUpdate?: (responseText: string) => void;
1618
- model?: string;
1619
- thinking?: string;
1620
- isolated?: boolean;
1621
- context?: Context["messages"];
1622
- },
1623
- ): Promise<{ responseText: string; stopReason: string }> {
1624
- const cwd = process.cwd();
1625
- const requestedModel = options?.model ?? "opus";
1626
- const model = resolveModel(requestedModel);
1627
- const modelId = model?.id ?? requestedModel;
1628
- const cliModel = model ? claudeCodeModelId(model, longContextSettings) : modelId;
1629
-
1630
- // Session resume for shared mode — reuse provider's session if it exists,
1631
- // otherwise create one from pi's context.
1632
- // Note: doesn't update sharedSession.cursor after completion, so the next
1633
- // provider call will see missed messages and trigger a Case 4 rebuild.
1634
- let resumeSessionId: string | null = null;
1635
- if (!options?.isolated && options?.context?.length) {
1636
- if (sharedSession) {
1637
- // Provider already has a session — just resume from it
1638
- // Any missed messages from other providers were already handled by the provider's Case 4
1639
- resumeSessionId = sharedSession.sessionId;
1640
- } else {
1641
- // No provider session yet — create one from pi's context
1642
- const contextWithPrompt = [...options.context, { role: "user" as const, content: prompt, timestamp: Date.now() }];
1643
- const sync = syncSharedSession(contextWithPrompt as Context["messages"], cwd, undefined, cliModel);
1644
- resumeSessionId = sync.sessionId;
1645
- }
1646
- }
1647
-
1648
- // Mode → disallowed tools
1649
- const disallowedTools = MODE_DISALLOWED_TOOLS[mode] ?? [];
1650
-
1651
- // Skills append
1652
- const skillsBlock = options?.appendSkills !== false && options?.systemPrompt
1653
- ? extractSkillsBlock(options.systemPrompt) : undefined;
1654
-
1655
- // Effort
1656
- const effort = options?.thinking && options.thinking !== "off"
1657
- ? REASONING_TO_EFFORT[options.thinking] : undefined;
1658
-
1659
- const claudeExecutable = providerSettings.pathToClaudeCodeExecutable;
1660
-
1661
- const extraArgs: Record<string, string | null> = {
1662
- "strict-mcp-config": null,
1663
- model: cliModel,
1664
- };
1665
- if (effort) extraArgs["thinking-display"] = "summarized";
1666
-
1667
- debug("askClaude:",
1668
- `mode=${mode} model=${modelId} cliModel=${cliModel} effort=${effort ?? "default"}`,
1669
- `isolated=${options?.isolated ?? false} resume=${resumeSessionId?.slice(0, 8) ?? "none"}`,
1670
- `skills=${Boolean(skillsBlock)} promptLen=${prompt.length}`);
1671
- const childEnv = await resolveClaudeChildEnv(piModelRegistry);
1672
-
1673
- const sdkQuery = query({
1674
- prompt,
1675
- options: {
1676
- cwd,
1677
- env: childEnv,
1678
- permissionMode: "bypassPermissions",
1679
- settings: claudeCodeSettings(providerSettings),
1680
- ...(disallowedTools.length ? { disallowedTools } : {}),
1681
- ...(effort ? { effort } : {}),
1682
- systemPrompt: skillsBlock
1683
- ? { type: "preset", preset: "claude_code", append: skillsBlock }
1684
- : undefined,
1685
- settingSources: ["user", "project"] as SettingSource[],
1686
- extraArgs,
1687
- ...(resumeSessionId ? { resume: resumeSessionId } : {}),
1688
- ...(options?.isolated ? { persistSession: false } : {}),
1689
- ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
1690
- ...makeCliDebugOptions("askclaude"),
1691
- },
1692
- });
1693
-
1694
- // Abort handling
1695
- let wasAborted = false;
1696
- const onAbort = () => {
1697
- wasAborted = true;
1698
- sdkQuery.interrupt().catch(() => { try { sdkQuery.close(); } catch {} });
1699
- };
1700
- if (signal?.aborted) { onAbort(); throw new Error("Aborted"); }
1701
- signal?.addEventListener("abort", onAbort, { once: true });
1702
-
1703
- let responseText = "";
1704
- let sdkMessageCount = 0;
1705
- let textDeltaCount = 0;
1706
- let resultSubtype: string | undefined;
1707
-
1708
- try {
1709
- for await (const message of sdkQuery) {
1710
- if (wasAborted) break;
1711
- sdkMessageCount++;
1712
-
1713
- switch (message.type) {
1714
- case "stream_event": {
1715
- const event = (message as SDKMessage & { event: any }).event;
1716
- // Text deltas → accumulate and stream
1717
- if (event?.type === "content_block_delta" && event.delta?.type === "text_delta") {
1718
- responseText += event.delta.text;
1719
- textDeltaCount++;
1720
- options?.onStreamUpdate?.(responseText);
1721
- }
1722
- // Tool call start → track for action summary progress
1723
- if (event?.type === "content_block_start" && event.content_block?.type === "tool_use") {
1724
- debug(`askClaude: tool_use start: ${event.content_block.name}`);
1725
- toolCalls.set(event.content_block.id, {
1726
- name: mapToolName(event.content_block.name),
1727
- status: "running",
1728
- });
1729
- }
1730
- break;
1731
- }
1732
- case "assistant": {
1733
- // Update tool calls with full input for action summary
1734
- for (const block of (message as any).message?.content ?? []) {
1735
- if (block.type === "tool_use") {
1736
- toolCalls.set(block.id, {
1737
- name: mapToolName(block.name),
1738
- status: "complete",
1739
- rawInput: block.input,
1740
- });
1741
- }
1742
- }
1743
- break;
1744
- }
1745
- case "result": {
1746
- resultSubtype = message.subtype;
1747
- const r = message as any;
1748
- if (r.usage) {
1749
- debug(`askClaude: result usage: in=${r.usage.input_tokens} out=${r.usage.output_tokens} cacheRead=${r.usage.cache_read_input_tokens ?? 0} cacheWrite=${r.usage.cache_creation_input_tokens ?? 0} turns=${r.num_turns ?? "?"}`);
1750
- }
1751
- if (!responseText && message.subtype === "success" && message.result) {
1752
- responseText = message.result;
1753
- }
1754
- break;
1755
- }
1756
- }
1757
- }
1758
-
1759
- const stopReason = wasAborted ? "cancelled" : "stop";
1760
- debug(`askClaude: done`,
1761
- `stopReason=${stopReason} resultSubtype=${resultSubtype ?? "none"}`,
1762
- `sdkMessages=${sdkMessageCount} textDeltas=${textDeltaCount} responseLen=${responseText.length}`,
1763
- `toolCalls=${toolCalls.size}`);
1764
- return { responseText, stopReason };
1765
- } finally {
1766
- signal?.removeEventListener("abort", onAbort);
1767
- sdkQuery.close();
1768
- }
1769
- }
1770
1557
 
1771
1558
  // --- Extension registration ---
1772
1559
 
1773
- const DEFAULT_TOOL_DESCRIPTION_FULL = "Delegate to Claude Code for a second opinion or analysis (code review, architecture questions, debugging theories), or to autonomously handle a task. Defaults to read-only mode — use full mode when the user wants to delegate a task that requires changes. Prefer to handle straightforward tasks yourself.";
1774
- const DEFAULT_TOOL_DESCRIPTION = "Delegate to Claude Code for a second opinion or analysis (code review, architecture questions, debugging theories). Read-only — Claude Code can explore the codebase but not make changes. Prefer to handle straightforward tasks yourself.";
1775
-
1776
- const PREVIEW_MAX_CHARS = 1000;
1777
- const PREVIEW_MAX_LINES = 6;
1778
-
1779
- let askClaudeToolName = "AskClaude";
1780
1560
 
1781
1561
  export default function (pi: ExtensionAPI) {
1782
1562
  // Disable non-essential Claude Code traffic (update checks, MCP registry, telemetry)
@@ -1787,7 +1567,7 @@ export default function (pi: ExtensionAPI) {
1787
1567
  providerSettings = config.provider ?? {};
1788
1568
  // We need these settings to know if we're eligible for 1M context on certain models
1789
1569
  longContextSettings = {
1790
- plan: providerSettings.plan ?? "pro",
1570
+ plan: providerSettings.plan ?? "max",
1791
1571
  longContextExtraUsage: providerSettings.longContextExtraUsage ?? false,
1792
1572
  };
1793
1573
  const registeredModels = applyLongContext(MODELS, longContextSettings);
@@ -1851,7 +1631,7 @@ export default function (pi: ExtensionAPI) {
1851
1631
  const msg = errorMessage(err);
1852
1632
  debug("session_before_compact: takeover failed; cancelling to avoid native compact fallback", err);
1853
1633
  ctx.ui?.notify?.(
1854
- `Claude bridge compact failed (${msg}); cancelled to avoid known hang. Retry, switch model, or reduce context.`,
1634
+ `pi-claude-agent-sdk compact failed (${msg}); cancelled to avoid known hang. Retry, switch model, or reduce context.`,
1855
1635
  "error",
1856
1636
  );
1857
1637
  return { cancel: true };
@@ -1900,133 +1680,4 @@ export default function (pi: ExtensionAPI) {
1900
1680
  // route through the parent's streamSimple via reentrant QueryContexts.
1901
1681
  debug(`provider: skipping re-registration, parent instance active (module=${moduleInstanceId})`);
1902
1682
  }
1903
-
1904
- // --- AskClaude tool ---
1905
-
1906
- const askConf = config.askClaude;
1907
- const allowFull = askConf?.allowFullMode !== false;
1908
- const defaultMode = askConf?.defaultMode ?? "read";
1909
- const defaultIsolated = askConf?.defaultIsolated ?? false;
1910
- askClaudeToolName = askConf?.name ?? "AskClaude";
1911
-
1912
- const modeValues = allowFull ? ["read", "full", "none"] as const : ["read", "none"] as const;
1913
- let modeDesc = `"read" (default): questions about the codebase — review, analysis, explain. "none": general knowledge only (no file access).`;
1914
- if (allowFull) modeDesc += ` "full": allows writing and bash execution (careful: runs without feedback to pi).`;
1915
-
1916
- if (askConf?.enabled !== false) {
1917
- const askClaudeParams = Type.Object({
1918
- prompt: Type.String({ description: "The question or task for Claude Code. By default Claude sees the full conversation history. Don't research up front, let Claude explore." }),
1919
- mode: Type.Optional(StringEnum(modeValues, { description: modeDesc })),
1920
- model: Type.Optional(Type.String({ description: 'Claude model (e.g. "opus", "sonnet", "haiku", or full ID). Defaults to "opus".' })),
1921
- thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const, { description: "Thinking effort level. Omit to use Claude Code's default." })),
1922
- isolated: Type.Optional(Type.Boolean({ description: "When true, Claude sees only this prompt (clean session). When false (default), Claude sees the full conversation history." })),
1923
- });
1924
- pi.registerTool<typeof askClaudeParams>({
1925
- name: askConf?.name ?? "AskClaude",
1926
- label: askConf?.label ?? "Ask Claude Code",
1927
- description: askConf?.description ?? (allowFull ? DEFAULT_TOOL_DESCRIPTION_FULL : DEFAULT_TOOL_DESCRIPTION),
1928
- parameters: askClaudeParams,
1929
- renderCall(args, theme) {
1930
- let text = theme.fg("mdLink", theme.bold("AskClaude "));
1931
- const mode = args.mode ?? defaultMode;
1932
- const tags: string[] = [];
1933
- if (mode !== defaultMode) tags.push(`mode=${mode}`);
1934
- if (args.model) tags.push(`model=${args.model}`);
1935
- if (args.thinking) tags.push(`thinking=${args.thinking}`);
1936
- if (args.isolated) tags.push("isolated");
1937
- if (tags.length) text += `${theme.fg("accent", `[${tags.join(", ")}]`)} `;
1938
- const truncated = args.prompt.length > PREVIEW_MAX_CHARS ? args.prompt.substring(0, PREVIEW_MAX_CHARS) : args.prompt;
1939
- const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
1940
- text += theme.fg("muted", `"${lines.join("\n")}"`);
1941
- if (args.prompt.length > PREVIEW_MAX_CHARS || args.prompt.split("\n").length > PREVIEW_MAX_LINES) text += theme.fg("dim", " …");
1942
- return new Text(text, 0, 0);
1943
- },
1944
- renderResult(result, { expanded, isPartial }, theme) {
1945
- if (isPartial) {
1946
- const status = result.content[0]?.type === "text" ? result.content[0].text : "working...";
1947
- return new Text(theme.fg("mdLink", "◉ Claude Code ") + theme.fg("muted", status), 0, 0);
1948
- }
1949
-
1950
- const details = result.details as { prompt?: string; executionTime?: number; actions?: string; error?: boolean } | undefined;
1951
- const body = result.content[0]?.type === "text" ? result.content[0].text : "";
1952
-
1953
- let text = details?.error
1954
- ? theme.fg("error", "✗ Claude Code error")
1955
- : theme.fg("mdLink", "✓ Claude Code");
1956
-
1957
- if (details?.executionTime) text += ` ${theme.fg("dim", `${(details.executionTime / 1000).toFixed(1)}s`)}`;
1958
- if (details?.actions) text += ` ${theme.fg("muted", details.actions)}`;
1959
-
1960
- if (expanded) {
1961
- if (details?.prompt) text += `\n${theme.fg("dim", `Prompt: ${details.prompt}`)}`;
1962
- if (details?.prompt && body) text += `\n${theme.fg("dim", "─".repeat(40))}`;
1963
- if (body) text += `\n${theme.fg("toolOutput", body)}`;
1964
- } else {
1965
- const truncated = body.length > PREVIEW_MAX_CHARS ? body.substring(0, PREVIEW_MAX_CHARS) : body;
1966
- const lines = truncated.split("\n").slice(0, PREVIEW_MAX_LINES);
1967
- if (lines.length) text += `\n${theme.fg("toolOutput", lines.join("\n"))}`;
1968
- if (body.length > PREVIEW_MAX_CHARS || body.split("\n").length > PREVIEW_MAX_LINES) text += `\n${theme.fg("dim", `… (${keyHint("app.tools.expand", "to expand")})`)}`;
1969
-
1970
- }
1971
-
1972
- return new Text(text, 0, 0);
1973
- },
1974
- async execute(_id, params, signal, onUpdate, ctx) {
1975
- // Guard: circular delegation
1976
- if (ctx.model?.baseUrl === "claude-bridge") {
1977
- debug("askClaude: blocked circular delegation (active provider is claude-bridge)");
1978
- return {
1979
- content: [{ type: "text" as const, text: "Error: AskClaude cannot be used when the active provider is claude-bridge — you're already running through Claude Code." }],
1980
- details: { error: true },
1981
- };
1982
- }
1983
-
1984
- const mode = (params.mode ?? defaultMode) as "full" | "read" | "none";
1985
- const isolated = params.isolated ?? defaultIsolated;
1986
- const toolCalls = new Map<string, ToolCallState>();
1987
- const start = Date.now();
1988
-
1989
- const progressInterval = setInterval(() => {
1990
- const elapsed = ((Date.now() - start) / 1000).toFixed(0);
1991
- const summary = buildActionSummary(toolCalls);
1992
- const status = summary ? `${elapsed}s — ${summary}` : `${elapsed}s — working...`;
1993
- onUpdate?.({
1994
- content: [{ type: "text", text: status }],
1995
- details: { prompt: params.prompt, executionTime: Date.now() - start },
1996
- });
1997
- }, 1000);
1998
-
1999
- try {
2000
- const result = await promptAndWait(params.prompt, mode, toolCalls, signal, {
2001
- systemPrompt: ctx.getSystemPrompt(),
2002
- appendSkills: askConf?.appendSkills,
2003
- model: params.model,
2004
- thinking: params.thinking,
2005
- isolated,
2006
- context: isolated ? undefined : buildSessionContext(ctx.sessionManager.getBranch()).messages as Context["messages"],
2007
- });
2008
- clearInterval(progressInterval);
2009
- onUpdate?.({ content: [{ type: "text", text: "" }], details: {} });
2010
- const executionTime = Date.now() - start;
2011
- const actions = buildActionSummary(toolCalls);
2012
-
2013
- const text = actions
2014
- ? `${result.responseText}\n\n[Claude Code actions: ${actions}]`
2015
- : result.responseText;
2016
- return {
2017
- content: [{ type: "text" as const, text }],
2018
- details: { prompt: params.prompt, executionTime, actions },
2019
- };
2020
- } catch (err) {
2021
- clearInterval(progressInterval);
2022
- debug(`askClaude error: mode=${mode}, model=${params.model ?? "default"}, isolated=${isolated}, elapsed=${((Date.now() - start) / 1000).toFixed(1)}s, error=`, err);
2023
- const msg = errorMessage(err);
2024
- return {
2025
- content: [{ type: "text" as const, text: `Error: ${msg}` }],
2026
- details: { prompt: params.prompt, executionTime: Date.now() - start, error: true },
2027
- };
2028
- }
2029
- },
2030
- });
2031
- }
2032
1683
  }
@@ -1,90 +0,0 @@
1
- // Status-line rendering helpers for the AskClaude tool.
2
- //
3
- // While Claude Code runs inside an AskClaude call, the pi TUI can't surface
4
- // each tool_use individually — there's only one status row for the whole
5
- // delegation. These helpers shape a tool_use record into a short, path-aware
6
- // label (e.g. "Read(src/foo.ts)", "Bash(git log --oneline…)") and collapse
7
- // runs of the same tool so the line doesn't flicker. Used only by
8
- // promptAndWait; the provider path exposes tools directly through pi's TUI
9
- // and doesn't need this.
10
-
11
- export interface ToolCallState {
12
- name: string;
13
- status: string;
14
- rawInput?: unknown;
15
- }
16
-
17
- export function extractPath(rawInput: unknown): string | undefined {
18
- if (!rawInput || typeof rawInput !== "object") return undefined;
19
- const input = rawInput as Record<string, unknown>;
20
- if (typeof input.file_path === "string") return input.file_path;
21
- if (typeof input.path === "string") return input.path;
22
- if (typeof input.command === "string") return input.command.substring(0, 80);
23
- return undefined;
24
- }
25
-
26
- export function shortPath(p: string): string {
27
- const cwd = process.cwd();
28
- if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
29
- if (p.startsWith("/")) {
30
- const parts = p.split("/");
31
- if (parts.length > 3) return parts.slice(-2).join("/");
32
- }
33
- return p;
34
- }
35
-
36
- export function formatToolAction(tc: ToolCallState): string | undefined {
37
- const path = extractPath(tc.rawInput);
38
- const verb = tc.name.toLowerCase().split(/\s/)[0];
39
- if (verb === "read" || verb === "readfile") {
40
- return path ? `Read(${shortPath(path)})` : "Read";
41
- } else if (verb === "glob") {
42
- const input = tc.rawInput as Record<string, unknown> | undefined;
43
- const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
44
- return pat ? `Glob(${pat})` : "Glob";
45
- } else if (verb === "edit" || verb === "write" || verb === "writefile" || verb === "multiedit") {
46
- return path ? `Edit(${shortPath(path)})` : "Edit";
47
- } else if (verb === "bashoutput") {
48
- return undefined; // redundant with preceding Bash call
49
- } else if (verb === "bash" || verb === "terminal") {
50
- return path ? `Bash(${path})` : "Bash";
51
- } else if (verb === "agent") {
52
- const input = tc.rawInput as Record<string, unknown> | undefined;
53
- return `Agent(${String(input?.description ?? "").slice(0, 40)})`;
54
- } else if (verb === "grep") {
55
- const input = tc.rawInput as Record<string, unknown> | undefined;
56
- const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
57
- return pat ? `Grep(${pat})` : "Grep";
58
- } else if (verb === "skill") {
59
- const input = tc.rawInput as Record<string, unknown> | undefined;
60
- const name = typeof input?.skill === "string" ? input.skill.slice(0, 40) : "";
61
- return name ? `Skill(${name})` : "Skill";
62
- } else if (verb === "todowrite" || verb === "taskcreate" || verb === "taskupdate") {
63
- const todos = Array.isArray((tc.rawInput as any)?.todos) ? (tc.rawInput as any).todos : [];
64
- const current = todos.find((t: any) => t.status === "in_progress") ?? todos.find((t: any) => t.status === "pending");
65
- const label = current ? String(current.content ?? "").slice(0, 40) : "";
66
- return label || undefined;
67
- } else if (verb === "askclaude") {
68
- // Recursive — don't show AskClaude in its own action summary
69
- return undefined;
70
- }
71
- return tc.name;
72
- }
73
-
74
- export function buildActionSummary(calls: Map<string, ToolCallState>): string {
75
- const parts: string[] = [];
76
- let prevVerb = "";
77
- for (const [, tc] of calls) {
78
- const action = formatToolAction(tc);
79
- if (!action) continue;
80
- const verb = tc.name.toLowerCase().split(/\s/)[0];
81
- // Collapse consecutive calls to the same tool — keep only the latest
82
- if (verb === prevVerb) {
83
- parts[parts.length - 1] = action;
84
- } else {
85
- parts.push(action);
86
- }
87
- prevVerb = verb;
88
- }
89
- return parts.join("; ");
90
- }