diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index f907d4b4..e7639593 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -246,7 +246,7 @@ export type TranscriptEntry = | { kind: "thinking"; ts: string; text: string; delta?: boolean } | { kind: "user"; ts: string; text: string } | { kind: "tool_call"; ts: string; name: string; input: unknown; toolUseId?: string } - | { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean } + | { kind: "tool_result"; ts: string; toolUseId: string; toolName?: string; content: string; isError: boolean } | { kind: "init"; ts: string; model: string; sessionId: string } | { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] } | { kind: "stderr"; ts: string; text: string } diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 27dce547..697387cd 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -26,6 +26,7 @@ import { ensurePiModelConfiguredAndAvailable } from "./models.js"; const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips"); +const PI_AGENT_SKILLS_DIR = path.join(os.homedir(), ".pi", "agent", "skills"); function firstNonEmptyLine(text: string): string { return ( @@ -56,35 +57,35 @@ function resolvePiBiller(env: Record, provider: string | null): async function ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) { const skillsEntries = await listPaperclipSkillEntries(__moduleDir); + + await fs.mkdir(PI_AGENT_SKILLS_DIR, { recursive: true }); if (skillsEntries.length === 0) return; - const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills"); - await fs.mkdir(piSkillsHome, { recursive: true }); const removedSkills = await removeMaintainerOnlySkillSymlinks( - piSkillsHome, + PI_AGENT_SKILLS_DIR, skillsEntries.map((entry) => entry.name), ); for (const skillName of removedSkills) { await onLog( "stderr", - `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${piSkillsHome}\n`, + `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${PI_AGENT_SKILLS_DIR}\n`, ); } for (const entry of skillsEntries) { - const target = path.join(piSkillsHome, entry.name); + const target = path.join(PI_AGENT_SKILLS_DIR, entry.name); try { const result = await ensurePaperclipSkillSymlink(entry.source, target); if (result === "skipped") continue; await onLog( "stderr", - `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.name}" into ${piSkillsHome}\n`, + `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.name}" into ${PI_AGENT_SKILLS_DIR}\n`, ); } catch (err) { await onLog( "stderr", - `[paperclip] Failed to inject Pi skill "${entry.name}" into ${piSkillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + `[paperclip] Failed to inject Pi skill "${entry.name}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`, ); } } @@ -336,6 +337,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) args.push(...extraArgs); return args; diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts index b80fe5f1..64a553e9 100644 --- a/packages/adapters/pi-local/src/ui/parse-stdout.ts +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -72,11 +72,22 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { for (const tr of toolResults) { const content = tr.content; const isError = tr.isError === true; - const contentStr = typeof content === "string" ? content : JSON.stringify(content); + + // Extract text from Pi's content array format + let contentStr: string; + if (typeof content === "string") { + contentStr = content; + } else if (Array.isArray(content)) { + contentStr = extractTextContent(content as Array<{ type: string; text?: string }>); + } else { + contentStr = JSON.stringify(content); + } + entries.push({ kind: "tool_result", ts, toolUseId: asString(tr.toolCallId, "unknown"), + toolName: asString(tr.toolName), content: contentStr, isError, }); @@ -130,14 +141,35 @@ export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] { if (type === "tool_execution_end") { const toolCallId = asString(parsed.toolCallId); + const toolName = asString(parsed.toolName); const result = parsed.result; const isError = parsed.isError === true; - const contentStr = typeof result === "string" ? result : JSON.stringify(result); + + // Extract text from Pi's content array format + // Can be: {"content": [{"type": "text", "text": "..."}]} or [{"type": "text", "text": "..."}] + let contentStr: string; + if (typeof result === "string") { + contentStr = result; + } else if (Array.isArray(result)) { + // Direct array format: result is [{"type": "text", "text": "..."}] + contentStr = extractTextContent(result as Array<{ type: string; text?: string }>); + } else if (result && typeof result === "object") { + const resultObj = result as Record; + if (Array.isArray(resultObj.content)) { + // Wrapped format: result is {"content": [{"type": "text", "text": "..."}]} + contentStr = extractTextContent(resultObj.content as Array<{ type: string; text?: string }>); + } else { + contentStr = JSON.stringify(result); + } + } else { + contentStr = JSON.stringify(result); + } return [{ kind: "tool_result", ts, toolUseId: toolCallId || "unknown", + toolName, content: contentStr, isError, }]; diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 3b3d53c0..69c31919 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -939,7 +939,7 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe /* ---- Internal sub-components ---- */ -const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "cursor"]); +const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor"]); /** Display list includes all real adapter types plus UI-only coming-soon entries. */ const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean }[] = [ diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx index 5f42ec0e..cd52dbc1 100644 --- a/ui/src/components/transcript/RunTranscriptView.tsx +++ b/ui/src/components/transcript/RunTranscriptView.tsx @@ -400,7 +400,7 @@ export function normalizeTranscript(entries: TranscriptEntry[], streaming: boole type: "tool", ts: entry.ts, endTs: entry.ts, - name: "tool", + name: entry.toolName ?? "tool", toolUseId: entry.toolUseId, input: null, result: entry.content,