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 +7 -50
- package/package.json +5 -7
- package/src/child-env.ts +1 -1
- package/src/config.ts +1 -12
- package/src/convert.ts +10 -10
- package/src/index.ts +15 -364
- package/src/askclaude-ui.ts +0 -90
package/README.md
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](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
|
|
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
|
|
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 `"
|
|
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
|
|
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 `
|
|
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.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider
|
|
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
|
|
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.
|
|
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.
|
|
13
|
-
//
|
|
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 —
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* - **Without a map
|
|
59
|
-
*
|
|
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,
|
|
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 {
|
|
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
|
|
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: "
|
|
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.
|
|
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
|
|
730
|
-
//
|
|
731
|
-
// rather than session_start: the notice persists a flag to the global
|
|
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
|
-
`
|
|
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
|
|
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(`
|
|
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
|
|
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 ?? "
|
|
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
|
-
`
|
|
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
|
}
|
package/src/askclaude-ui.ts
DELETED
|
@@ -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
|
-
}
|