BIG pi update with claude chat
This commit is contained in:
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
275
pi/.pi/agent/extensions/ask-claude.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* ask-claude — Stream Claude agent reviews into pi.
|
||||||
|
*
|
||||||
|
* For AGENTS to use. Delegates to specialized Claude agents or raw models
|
||||||
|
* for review, analysis, debugging, and second opinions.
|
||||||
|
*
|
||||||
|
* Tool (callable by the LLM):
|
||||||
|
* ask_claude(prompt, agent?, model?, question?, session_id?)
|
||||||
|
* agent — any agent name from ~/.claude/agents/ (e.g. "plan_review", "code_review", "oracle", "debug")
|
||||||
|
* model — model override: "opus", "sonnet", or full model ID
|
||||||
|
* question — specific focus prepended as a review header
|
||||||
|
* session_id — resume a prior conversation (returned in every response)
|
||||||
|
*
|
||||||
|
* Commands:
|
||||||
|
* /review-plan — editor → Claude Opus plan_review → inject result
|
||||||
|
* /review-code — editor → Claude Sonnet code_review → inject result
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineTool, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Type } from "@mariozechner/pi-ai";
|
||||||
|
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||||
|
import {
|
||||||
|
type ClaudeDetails,
|
||||||
|
type Theme,
|
||||||
|
renderToolCallLine,
|
||||||
|
renderToolResultBox,
|
||||||
|
renderToolBlock,
|
||||||
|
formatUsage,
|
||||||
|
buildEditDiff,
|
||||||
|
formatAnthropicError,
|
||||||
|
runClaude,
|
||||||
|
type RunClaudeResult,
|
||||||
|
} from "../shared/claude-stream.js";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rendering (ask-claude specific — uses shared tool renderers)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function buildLabel(agent?: string, model?: string): string {
|
||||||
|
if (agent) return `Claude [${agent}]`;
|
||||||
|
if (model) return `Claude [${model}]`;
|
||||||
|
return "Claude Sonnet";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tool definition
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const AskClaudeParams = Type.Object({
|
||||||
|
prompt: Type.String({
|
||||||
|
description: "Full content to review/analyze. Include all relevant context: CLAUDE.md conventions, files explored, code or plan to review.",
|
||||||
|
}),
|
||||||
|
agent: Type.Optional(Type.String({
|
||||||
|
description: "Agent name from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug'). Omit to use model= directly.",
|
||||||
|
})),
|
||||||
|
model: Type.Optional(Type.String({
|
||||||
|
description: "Model override: 'opus', 'sonnet', 'haiku', or a full model ID. When agent is set this overrides the agent's default. When agent is omitted this selects the model directly.",
|
||||||
|
})),
|
||||||
|
question: Type.Optional(Type.String({
|
||||||
|
description: "Specific question or focus area prepended to the prompt (e.g. 'Focus on security', 'Are there race conditions?').",
|
||||||
|
})),
|
||||||
|
session_id: Type.Optional(Type.String({
|
||||||
|
description: "Resume a prior conversation. Pass the session_id returned from a previous ask_claude call.",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const askClaudeTool = defineTool({
|
||||||
|
name: "ask_claude",
|
||||||
|
label: "Ask Claude",
|
||||||
|
description: [
|
||||||
|
"Ask a Claude agent or model for review, analysis, or a second opinion.",
|
||||||
|
"agent=<name> runs a named agent from ~/.claude/agents/ (e.g. 'plan_review', 'code_review', 'oracle', 'debug').",
|
||||||
|
"Use model= alone for free-form requests without an agent. Use question= to specify a focus.",
|
||||||
|
"Pass session_id from a prior response to continue the same conversation across turns.",
|
||||||
|
"CLAUDE.md and .claude/skills are loaded automatically from the project root.",
|
||||||
|
].join(" "),
|
||||||
|
promptSnippet: "Ask a Claude agent or model for review, analysis, or a second opinion",
|
||||||
|
promptGuidelines: [
|
||||||
|
"Use ask_claude(agent=<name>) to invoke a specialized agent — include all relevant context in the prompt.",
|
||||||
|
"Use ask_claude(model='opus', question='...') for free-form deep analysis.",
|
||||||
|
"Always include the artifact to review (plan, code, problem description) in the prompt.",
|
||||||
|
"Pass session_id back from the previous response to continue the conversation.",
|
||||||
|
],
|
||||||
|
parameters: AskClaudeParams,
|
||||||
|
|
||||||
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||||
|
const fullPrompt = params.question
|
||||||
|
? `## Review Focus\n\n${params.question}\n\n## Content\n\n${params.prompt}`
|
||||||
|
: params.prompt;
|
||||||
|
|
||||||
|
const label = buildLabel(params.agent, params.model);
|
||||||
|
const details: ClaudeDetails = { label, done: false, blocks: [], finalText: "", isResume: !!params.session_id };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: RunClaudeResult = await runClaude(fullPrompt, {
|
||||||
|
agent: params.agent,
|
||||||
|
model: params.model,
|
||||||
|
sessionId: params.session_id,
|
||||||
|
enrichEditDiffs: true, // ask-claude wants diff enrichment
|
||||||
|
cwd: ctx.cwd,
|
||||||
|
signal,
|
||||||
|
onUpdate: (partial) => {
|
||||||
|
Object.assign(details, partial);
|
||||||
|
onUpdate?.({
|
||||||
|
content: [{ type: "text", text: details.finalText || "(thinking…)" }],
|
||||||
|
details: { ...details },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
details.done = true;
|
||||||
|
details.finalText = result.finalText;
|
||||||
|
details.blocks = result.blocks;
|
||||||
|
details.sessionId = result.sessionId;
|
||||||
|
details.costUsd = result.costUsd;
|
||||||
|
details.inputTokens = result.inputTokens;
|
||||||
|
details.outputTokens = result.outputTokens;
|
||||||
|
details.cacheReadTokens = result.cacheReadTokens;
|
||||||
|
details.cacheWriteTokens = result.cacheWriteTokens;
|
||||||
|
|
||||||
|
const sessionNote = result.sessionId
|
||||||
|
? `\n\n---\n*session_id: \`${result.sessionId}\`*`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (result.finalText || "(no output)") + sessionNote }],
|
||||||
|
details: { ...details },
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
details.done = true;
|
||||||
|
details.finalText = `Error: ${errMsg}`;
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `**Claude error:** ${errMsg}` }],
|
||||||
|
details: { ...details },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCall(args, theme, _ctx) {
|
||||||
|
const label = buildLabel(args.agent, args.model);
|
||||||
|
let text = theme.fg("toolTitle", theme.bold("ask_claude ")) + theme.fg("accent", `[${label}]`);
|
||||||
|
if (args.question) {
|
||||||
|
text += "\n " + theme.fg("dim", theme.italic(args.question));
|
||||||
|
} else {
|
||||||
|
const lines = args.prompt.split("\n").filter((l) => l.trim()).slice(0, 3);
|
||||||
|
text += "\n " + theme.fg("dim", lines.join("\n "));
|
||||||
|
}
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResult(result, { isPartial }, theme, _ctx) {
|
||||||
|
const d = result.details as ClaudeDetails | undefined;
|
||||||
|
if (!d) return new Text(theme.fg("muted", "…"), 0, 0);
|
||||||
|
|
||||||
|
const isDone = d.done && !isPartial;
|
||||||
|
const statusIcon = isDone ? theme.fg("success", "✓ ") : theme.fg("warning", "⏳ ");
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
const resume = d.isResume ? theme.fg("dim", " ↩") : "";
|
||||||
|
c.addChild(new Text(statusIcon + theme.fg("toolTitle", theme.bold(d.label)) + resume, 0, 0));
|
||||||
|
|
||||||
|
for (const block of d.blocks ?? []) {
|
||||||
|
if (block.type === "thinking" && block.text.trim()) {
|
||||||
|
c.addChild(new Text(theme.fg("dim", theme.italic(block.text.trimEnd())), 0, 0));
|
||||||
|
} else if (block.type === "tool") {
|
||||||
|
c.addChild(renderToolBlock(block, theme as any));
|
||||||
|
} else if (block.type === "text" && block.text.trim()) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
if (isDone) {
|
||||||
|
c.addChild(new Markdown(block.text.trim(), 0, 0, getMarkdownTheme()));
|
||||||
|
} else {
|
||||||
|
c.addChild(new Text(theme.fg("dim", block.text.trimEnd()), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone) {
|
||||||
|
const usageLine = formatUsage(d);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (usageLine) parts.push(usageLine);
|
||||||
|
if (d.sessionId) parts.push(`session:${d.sessionId.slice(0, 8)}`);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Text(theme.fg("dim", parts.join(" ")), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Extension entry point
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.registerTool(askClaudeTool);
|
||||||
|
|
||||||
|
// ── /review-plan ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("review-plan", {
|
||||||
|
description: "Editor → Claude Opus plan_review → inject review into conversation",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
const input = await ctx.ui.editor(
|
||||||
|
"Plan Review · Claude Opus",
|
||||||
|
"Paste your plan or strategy. Claude will review for correctness, completeness, and risk.",
|
||||||
|
);
|
||||||
|
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||||
|
ctx.ui.setStatus("ask-claude", "Asking Claude Opus (plan_review)…");
|
||||||
|
try {
|
||||||
|
const r = await runClaude(input, { agent: "plan_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||||
|
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||||
|
pi.sendMessage(
|
||||||
|
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||||
|
details: { label: "Claude Opus · plan_review", output: r.finalText } },
|
||||||
|
{ triggerTurn: true },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||||
|
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── /review-code ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("review-code", {
|
||||||
|
description: "Editor → Claude Sonnet code_review → inject review into conversation",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
const input = await ctx.ui.editor(
|
||||||
|
"Code Review · Claude Sonnet",
|
||||||
|
"Paste code to review. Include the plan it implements and any specific concerns.",
|
||||||
|
);
|
||||||
|
if (!input?.trim()) { ctx.ui.notify("Cancelled.", "info"); return; }
|
||||||
|
ctx.ui.setStatus("ask-claude", "Asking Claude Sonnet (code_review)…");
|
||||||
|
try {
|
||||||
|
const r = await runClaude(input, { agent: "code_review", cwd: ctx.cwd, onUpdate: () => {} });
|
||||||
|
if (!r.finalText.trim()) { ctx.ui.notify("No output from Claude.", "warning"); return; }
|
||||||
|
pi.sendMessage(
|
||||||
|
{ customType: "ask-claude-review", content: r.finalText.trim(), display: true,
|
||||||
|
details: { label: "Claude Sonnet · code_review", output: r.finalText } },
|
||||||
|
{ triggerTurn: true },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.ui.notify(`Claude error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||||
|
} finally { ctx.ui.setStatus("ask-claude", undefined); }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Message renderer for injected reviews ──────────────────────────────
|
||||||
|
|
||||||
|
pi.registerMessageRenderer("ask-claude-review", (message, { expanded }, theme) => {
|
||||||
|
const d = message.details as { label?: string; output?: string } | undefined;
|
||||||
|
const label = d?.label ?? "Claude";
|
||||||
|
const output = (d?.output ?? "").trim();
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
const c = new Container();
|
||||||
|
c.addChild(new Text(theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label)), 0, 0));
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Markdown(output, 0, 0, getMarkdownTheme()));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = theme.fg("accent", "◆ ") + theme.fg("toolTitle", theme.bold(label));
|
||||||
|
const lines = output.split("\n").filter((l) => l.trim());
|
||||||
|
const preview = lines.slice(0, 4).join("\n");
|
||||||
|
if (preview) {
|
||||||
|
text += "\n" + theme.fg("dim", preview);
|
||||||
|
if (lines.length > 4) text += "\n" + theme.fg("muted", `… ${lines.length - 4} more (Ctrl+O)`);
|
||||||
|
}
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
1371
pi/.pi/agent/extensions/chat-claude.ts
Normal file
1371
pi/.pi/agent/extensions/chat-claude.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,419 +0,0 @@
|
|||||||
/**
|
|
||||||
* Claude Account Switch Extension
|
|
||||||
*
|
|
||||||
* Switches between two Claude Pro accounts (personal and work) **without
|
|
||||||
* restarting pi**. Works by swapping auth.json files at the filesystem level,
|
|
||||||
* then reloading the auth storage and forcing an immediate token refresh to
|
|
||||||
* validate the switch.
|
|
||||||
*
|
|
||||||
* Why file-level swaps? Anthropic's OAuth rotates refresh tokens on every
|
|
||||||
* refresh. Calling authStorage.set() can appear to work, but the next
|
|
||||||
* getApiKey() call triggers refreshOAuthTokenWithLock(), which re-reads
|
|
||||||
* auth.json from disk — overwriting in-memory changes if persistence
|
|
||||||
* silently failed. Working at the file level avoids this entirely.
|
|
||||||
*
|
|
||||||
* Setup (one-time per account):
|
|
||||||
* 1. /login → authenticate with personal account
|
|
||||||
* 2. /switch-claude save personal
|
|
||||||
* 3. /login → authenticate with work account
|
|
||||||
* 4. /switch-claude save work
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* /switch-claude — pick account interactively
|
|
||||||
* /switch-claude save <name> — save current pi login as a named profile
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import * as fs from "node:fs";
|
|
||||||
import * as os from "node:os";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { execSync } from "node:child_process";
|
|
||||||
|
|
||||||
const HOME = os.homedir();
|
|
||||||
const AUTH_JSON = path.join(HOME, ".pi/agent/auth.json");
|
|
||||||
const MARKER_FILE = path.join(HOME, ".pi/agent/auth.json.current");
|
|
||||||
const PROFILES_DIR = path.join(HOME, ".pi/agent/profiles");
|
|
||||||
|
|
||||||
type Account = "personal" | "work";
|
|
||||||
|
|
||||||
// ── Session-window helpers ─────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// We store the actual `resets_at` timestamp returned by Claude's usage API
|
|
||||||
// (via the usage:update event) so the switch menu can show a live countdown
|
|
||||||
// to the next session reset rather than a guessed switchedAt + 5h window.
|
|
||||||
|
|
||||||
function sessionStampPath(account: Account): string {
|
|
||||||
return path.join(PROFILES_DIR, `session-${account}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Persist the actual session-reset timestamp for an account. */
|
|
||||||
function saveSessionResetsAt(account: Account, resetsAt: number): void {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
sessionStampPath(account),
|
|
||||||
JSON.stringify({ resetsAt }, null, 2),
|
|
||||||
{ mode: 0o600 },
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load the stored session-reset timestamp (ms epoch), or null. */
|
|
||||||
function loadSessionResetsAt(account: Account): number | null {
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(sessionStampPath(account), "utf-8");
|
|
||||||
const { resetsAt } = JSON.parse(raw) as { resetsAt: number };
|
|
||||||
if (typeof resetsAt === "number") return resetsAt;
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format milliseconds as a compact duration string. */
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
const totalSec = Math.ceil(ms / 1000);
|
|
||||||
const h = Math.floor(totalSec / 3600);
|
|
||||||
const m = Math.floor((totalSec % 3600) / 60);
|
|
||||||
const s = totalSec % 60;
|
|
||||||
if (h > 0) return `${h}h ${m}m`;
|
|
||||||
if (m > 0) return `${m}m`;
|
|
||||||
return `${s}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One-line session summary appended to each account option in the select menu.
|
|
||||||
*
|
|
||||||
* Window still running: [resets in Xh Ym]
|
|
||||||
* Window already passed: [0 (ready)]
|
|
||||||
* Never recorded: (empty)
|
|
||||||
*/
|
|
||||||
function sessionSummary(account: Account): string {
|
|
||||||
const resetsAt = loadSessionResetsAt(account);
|
|
||||||
if (resetsAt === null) return "";
|
|
||||||
const remaining = resetsAt - Date.now();
|
|
||||||
if (remaining <= 0) return " [0 (ready)]";
|
|
||||||
return ` [resets in ${formatDuration(remaining)}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Profile helpers ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function profilePath(account: Account): string {
|
|
||||||
return path.join(PROFILES_DIR, `auth-${account}.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasProfile(account: Account): boolean {
|
|
||||||
return fs.existsSync(profilePath(account));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save auth.json content directly to a profile file.
|
|
||||||
* This captures the exact on-disk state, including any tokens that were
|
|
||||||
* refreshed behind our back by the auth system.
|
|
||||||
*
|
|
||||||
* We parse + re-serialize the JSON to guard against corrupt auth.json
|
|
||||||
* (e.g. trailing commas left by buggy serializers). If the file can't
|
|
||||||
* be parsed, we skip the save rather than propagate bad data.
|
|
||||||
*/
|
|
||||||
function saveCurrentAuthToProfile(account: Account): boolean {
|
|
||||||
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
||||||
if (!fs.existsSync(AUTH_JSON)) return false;
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(AUTH_JSON, "utf-8");
|
|
||||||
const parsed = JSON.parse(raw); // validates JSON
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
fs.writeFileSync(profilePath(account), clean, { mode: 0o600 });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
// auth.json is missing or corrupt — don't propagate bad data
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a profile file to auth.json. This is an atomic-ish swap that
|
|
||||||
* replaces the entire file rather than merging per-provider.
|
|
||||||
*
|
|
||||||
* Like saveCurrentAuthToProfile, we round-trip through JSON.parse to
|
|
||||||
* ensure we never write corrupt data to auth.json.
|
|
||||||
*/
|
|
||||||
function restoreProfileToAuth(account: Account): void {
|
|
||||||
const raw = fs.readFileSync(profilePath(account), "utf-8");
|
|
||||||
const parsed = JSON.parse(raw); // throws on corrupt profile
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
fs.writeFileSync(AUTH_JSON, clean, { mode: 0o600 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMarker(account: Account): void {
|
|
||||||
fs.writeFileSync(MARKER_FILE, account, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentAccount(): Account | "unknown" {
|
|
||||||
try {
|
|
||||||
const marker = fs.readFileSync(MARKER_FILE, "utf8").trim();
|
|
||||||
if (marker === "personal" || marker === "work") return marker;
|
|
||||||
} catch {}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Other session detection ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function otherPiSessions(): number[] {
|
|
||||||
try {
|
|
||||||
const myPid = process.pid;
|
|
||||||
// Use a character class [c] trick so pgrep doesn't match its own process
|
|
||||||
const out = execSync("pgrep -f 'pi-[c]oding-agent' 2>/dev/null || true", {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
const pids = out
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map(Number)
|
|
||||||
.filter((p) => p && p !== myPid && !isNaN(p));
|
|
||||||
return pids;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function killOtherSessions(pids: number[]): number {
|
|
||||||
let killed = 0;
|
|
||||||
for (const pid of pids) {
|
|
||||||
try {
|
|
||||||
process.kill(pid);
|
|
||||||
killed++;
|
|
||||||
} catch {
|
|
||||||
// already dead or permission denied
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return killed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── UI helpers ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function statusLabel(account: Account | "unknown"): string {
|
|
||||||
switch (account) {
|
|
||||||
case "personal":
|
|
||||||
return " personal";
|
|
||||||
case "work":
|
|
||||||
return " work";
|
|
||||||
default:
|
|
||||||
return " claude";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Extension ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
let currentAccount: Account | "unknown" = "unknown";
|
|
||||||
|
|
||||||
// Whenever usage-bars fetches fresh data, save the real resets_at for the
|
|
||||||
// current account so the switch menu shows an accurate live countdown.
|
|
||||||
pi.events.on("usage:update", (event: unknown) => {
|
|
||||||
const e = event as { sessionResetsAt?: number };
|
|
||||||
if (currentAccount !== "unknown" && typeof e.sessionResetsAt === "number") {
|
|
||||||
saveSessionResetsAt(currentAccount, e.sessionResetsAt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
|
||||||
// Proper-lockfile creates auth.json.lock as a *directory* (atomic mkdir).
|
|
||||||
// If a regular file exists at that path (e.g. left by an older pi version),
|
|
||||||
// rmdir fails with ENOTDIR → lock acquisition throws → loadError is set →
|
|
||||||
// credentials are never persisted after /login. Delete the stale file and
|
|
||||||
// reload so this session has working auth persistence.
|
|
||||||
const lockPath = AUTH_JSON + ".lock";
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(lockPath);
|
|
||||||
if (stat.isFile()) {
|
|
||||||
fs.unlinkSync(lockPath);
|
|
||||||
ctx.modelRegistry.authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// lock doesn't exist or we can't stat it — nothing to fix
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guard against corrupt auth.json (e.g. trailing commas from buggy
|
|
||||||
// serializers). Re-serialize to clean JSON and reload so the auth
|
|
||||||
// system picks up valid credentials.
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(AUTH_JSON, "utf-8");
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
const clean = JSON.stringify(parsed, null, 2);
|
|
||||||
if (clean !== raw) {
|
|
||||||
fs.writeFileSync(AUTH_JSON, clean, { mode: 0o600 });
|
|
||||||
ctx.modelRegistry.authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// auth.json missing or unparseable — nothing we can fix here
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAccount = getCurrentAccount();
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.registerCommand("switch-claude", {
|
|
||||||
description:
|
|
||||||
"Switch between personal () and work () Claude accounts. Use 'save <name>' to save current login as a profile.",
|
|
||||||
handler: async (args, ctx) => {
|
|
||||||
const authStorage = ctx.modelRegistry.authStorage;
|
|
||||||
const trimmed = args?.trim() ?? "";
|
|
||||||
|
|
||||||
// ── Save current auth state as a named profile ──────────────────
|
|
||||||
if (trimmed.startsWith("save ")) {
|
|
||||||
const name = trimmed.slice(5).trim();
|
|
||||||
if (name !== "personal" && name !== "work") {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"Usage: /switch-claude save personal|work",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authStorage.has("anthropic")) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"No Anthropic credentials found. Run /login first.",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCurrentAuthToProfile(name as Account);
|
|
||||||
currentAccount = name as Account;
|
|
||||||
setMarker(currentAccount);
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Saved current login as ${statusLabel(name as Account)} profile`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Resolve target account (direct arg or interactive) ──────────
|
|
||||||
let newAccount: Account;
|
|
||||||
if (trimmed === "personal" || trimmed === "work") {
|
|
||||||
newAccount = trimmed;
|
|
||||||
} else if (trimmed === "") {
|
|
||||||
const personalLabel = ` personal${currentAccount === "personal" ? " ← current" : ""}${!hasProfile("personal") ? " (no profile saved)" : ""}${sessionSummary("personal")}`;
|
|
||||||
const workLabel = ` work${currentAccount === "work" ? " ← current" : ""}${!hasProfile("work") ? " (no profile saved)" : ""}${sessionSummary("work")}`;
|
|
||||||
|
|
||||||
const accountChoice = await ctx.ui.select(
|
|
||||||
"Switch Claude account:",
|
|
||||||
[personalLabel, workLabel],
|
|
||||||
);
|
|
||||||
if (accountChoice === undefined) return;
|
|
||||||
newAccount = accountChoice.startsWith("") ? "personal" : "work";
|
|
||||||
} else {
|
|
||||||
ctx.ui.notify(
|
|
||||||
"Usage: /switch-claude [personal|work|save <name>]",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAccount === currentAccount) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Already using ${statusLabel(newAccount)}`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Warn about other sessions ───────────────────────────────────
|
|
||||||
const otherPids = otherPiSessions();
|
|
||||||
if (otherPids.length > 0) {
|
|
||||||
const sessionChoice = await ctx.ui.select(
|
|
||||||
`⚠️ ${otherPids.length} other pi session(s) detected`,
|
|
||||||
[
|
|
||||||
"Continue anyway",
|
|
||||||
`Kill ${otherPids.length} other instance(s) and continue`,
|
|
||||||
"Cancel",
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (sessionChoice === undefined || sessionChoice.includes("Cancel"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (sessionChoice.includes("Kill")) {
|
|
||||||
const killed = killOtherSessions(otherPids);
|
|
||||||
ctx.ui.notify(`Killed ${killed} pi session(s)`, "info");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasProfile(newAccount)) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`No profile saved for ${newAccount}. Run /login then /switch-claude save ${newAccount}`,
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Perform the switch ──────────────────────────────────────────
|
|
||||||
try {
|
|
||||||
// 1. Snapshot current auth.json → outgoing profile.
|
|
||||||
// This captures any tokens that were silently refreshed
|
|
||||||
// since the last save (the file is the source of truth,
|
|
||||||
// not the in-memory snapshot from getAll()).
|
|
||||||
if (currentAccount !== "unknown") {
|
|
||||||
saveCurrentAuthToProfile(currentAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Copy incoming profile → auth.json (full file replace).
|
|
||||||
restoreProfileToAuth(newAccount);
|
|
||||||
|
|
||||||
// 3. Tell AuthStorage to re-read the file. This updates
|
|
||||||
// the in-memory credential cache from the new auth.json.
|
|
||||||
authStorage.reload();
|
|
||||||
|
|
||||||
// 4. Force an immediate token refresh to validate the switch.
|
|
||||||
// If the stored refresh token is stale, this will fail now
|
|
||||||
// rather than on the next chat message.
|
|
||||||
const apiKey = await authStorage.getApiKey("anthropic");
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
// Refresh failed → roll back to the previous account.
|
|
||||||
if (currentAccount !== "unknown") {
|
|
||||||
restoreProfileToAuth(currentAccount);
|
|
||||||
authStorage.reload();
|
|
||||||
}
|
|
||||||
ctx.ui.notify(
|
|
||||||
`❌ Switch failed: could not authenticate as ${newAccount}. ` +
|
|
||||||
`The saved refresh token may have expired. ` +
|
|
||||||
`Run /login then /switch-claude save ${newAccount} to re-save.`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Success — the refresh worked, auth.json now has fresh
|
|
||||||
// tokens. Save them back to the profile so next switch
|
|
||||||
// has the latest refresh token.
|
|
||||||
saveCurrentAuthToProfile(newAccount);
|
|
||||||
|
|
||||||
currentAccount = newAccount;
|
|
||||||
setMarker(currentAccount);
|
|
||||||
ctx.ui.setStatus("claude-account", statusLabel(currentAccount));
|
|
||||||
pi.events.emit("claude-account:switched", { account: newAccount });
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Switched to ${statusLabel(newAccount)} ✓`,
|
|
||||||
"info",
|
|
||||||
);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
// Something went wrong → try to roll back.
|
|
||||||
try {
|
|
||||||
if (currentAccount !== "unknown" && hasProfile(currentAccount)) {
|
|
||||||
restoreProfileToAuth(currentAccount);
|
|
||||||
authStorage.reload();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// rollback failed too — nothing more we can do
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
ctx.ui.notify(
|
|
||||||
`❌ Switch failed: ${msg}. Rolled back to ${statusLabel(currentAccount)}. ` +
|
|
||||||
`You may need to /login and /switch-claude save ${newAccount}.`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,10 @@
|
|||||||
* Replaces the built-in pi footer with a single clean line that assembles
|
* Replaces the built-in pi footer with a single clean line that assembles
|
||||||
* status from all other extensions:
|
* status from all other extensions:
|
||||||
*
|
*
|
||||||
* \uEF85 | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
* ~dir | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
||||||
*
|
*
|
||||||
* Status sources:
|
* Status sources:
|
||||||
* "claude-account" — set by claude-account-switch.ts → just the icon
|
* usage:update event — set by usage-bars extension → S/W bars (Claude usage, always shown)
|
||||||
* "usage-bars" — set by usage-bars extension → S/W bars (pass-through)
|
|
||||||
* ctx.getContextUsage() → C bar (rendered here)
|
* ctx.getContextUsage() → C bar (rendered here)
|
||||||
* ctx.model → model short name
|
* ctx.model → model short name
|
||||||
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
||||||
@@ -77,11 +76,6 @@ function formatDurationMs(ms: number): string {
|
|||||||
return "<1m";
|
return "<1m";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nerd Font codepoints matched to what claude-account-switch.ts emits
|
|
||||||
const ICON_PERSONAL = "\uEF85"; // U+EF85 — home
|
|
||||||
const ICON_WORK = "\uDB80\uDCD6"; // U+F00D6 — briefcase (surrogate pair)
|
|
||||||
const ICON_UNKNOWN = "\uF420"; // U+F420 — claude default
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
let ctx: any = null;
|
let ctx: any = null;
|
||||||
let tuiRef: any = null;
|
let tuiRef: any = null;
|
||||||
@@ -112,18 +106,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
: cwd;
|
: cwd;
|
||||||
parts.push(theme.fg("dim", dir));
|
parts.push(theme.fg("dim", dir));
|
||||||
|
|
||||||
// 2. Account icon
|
// 2. S / W usage bars + C bar — joined as one |-separated block
|
||||||
const acctRaw = statuses.get("claude-account");
|
|
||||||
if (acctRaw !== undefined) {
|
|
||||||
const clean = stripAnsi(acctRaw).trim();
|
|
||||||
let icon: string;
|
|
||||||
if (clean.includes("personal")) icon = ICON_PERSONAL;
|
|
||||||
else if (clean.includes("work")) icon = ICON_WORK;
|
|
||||||
else icon = ICON_UNKNOWN;
|
|
||||||
parts.push(theme.fg("dim", icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. S / W usage bars + C bar — joined as one |-separated block
|
|
||||||
const usageRaw = statuses.get("usage-bars");
|
const usageRaw = statuses.get("usage-bars");
|
||||||
const contextUsage = ctx?.getContextUsage?.();
|
const contextUsage = ctx?.getContextUsage?.();
|
||||||
{
|
{
|
||||||
@@ -135,8 +118,8 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const session = Math.max(0, Math.min(100, Math.round(usageSession)));
|
const session = Math.max(0, Math.min(100, Math.round(usageSession)));
|
||||||
const weekly = Math.max(0, Math.min(100, Math.round(usageWeekly)));
|
const weekly = Math.max(0, Math.min(100, Math.round(usageWeekly)));
|
||||||
|
|
||||||
let sPart = theme.fg("muted", "S ") + renderBrailleBar(theme, session) + " " + theme.fg("dim", `${session}%`);
|
let sPart = theme.fg("muted", "\uF4F5 S ") + renderBrailleBar(theme, session) + " " + theme.fg("dim", `${session}%`);
|
||||||
let wPart = theme.fg("muted", "W ") + renderBrailleBar(theme, weekly) + " " + theme.fg("dim", `${weekly}%`);
|
let wPart = theme.fg("muted", "\uF4F5 W ") + renderBrailleBar(theme, weekly) + " " + theme.fg("dim", `${weekly}%`);
|
||||||
|
|
||||||
if (sessionResetsAt !== null) {
|
if (sessionResetsAt !== null) {
|
||||||
const msLeft = sessionResetsAt - Date.now();
|
const msLeft = sessionResetsAt - Date.now();
|
||||||
@@ -156,34 +139,51 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
if (contextUsage && contextUsage.percent !== null) {
|
if (contextUsage && contextUsage.percent !== null) {
|
||||||
const pct = Math.round(contextUsage.percent);
|
const pct = Math.round(contextUsage.percent);
|
||||||
|
const chatStatus = statuses.get("chat-claude");
|
||||||
|
const isChatActive = !!chatStatus && chatStatus.includes("Claude");
|
||||||
|
|
||||||
|
// When chat is active and context is high, show warning indicator
|
||||||
|
let cLabel = "C";
|
||||||
|
let cColor = "muted";
|
||||||
|
if (isChatActive && pct >= 70) {
|
||||||
|
cLabel = pct >= 90 ? "C⚠" : "C⚡";
|
||||||
|
cColor = pct >= 90 ? "error" : "warning";
|
||||||
|
}
|
||||||
|
|
||||||
const cBar =
|
const cBar =
|
||||||
theme.fg("muted", "C ") +
|
theme.fg(cColor, cLabel + " ") +
|
||||||
renderBrailleBar(theme, pct) +
|
renderBrailleBar(theme, pct) +
|
||||||
" " +
|
" " +
|
||||||
theme.fg("dim", `${pct}%`);
|
theme.fg(pct >= 70 && isChatActive ? "warning" : "dim", `${pct}%`);
|
||||||
block = block ? block + pipeSep + cBar : cBar;
|
block = block ? block + pipeSep + cBar : cBar;
|
||||||
}
|
}
|
||||||
if (block) parts.push(block);
|
if (block) parts.push(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Model short name
|
// 3. Model short name
|
||||||
const modelId = ctx?.model?.id;
|
const modelId = ctx?.model?.id;
|
||||||
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
||||||
|
|
||||||
// 5. LSP — strip "LSP" prefix and activity dot
|
// 4. LSP — strip "LSP" prefix and activity dot
|
||||||
const lspRaw = statuses.get("lsp");
|
const lspRaw = statuses.get("lsp");
|
||||||
if (lspRaw) {
|
if (lspRaw) {
|
||||||
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
if (clean) parts.push(theme.fg("dim", clean));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. MCP — strip " servers" suffix
|
// 5. MCP — strip " servers" suffix
|
||||||
const mcpRaw = statuses.get("mcp");
|
const mcpRaw = statuses.get("mcp");
|
||||||
if (mcpRaw) {
|
if (mcpRaw) {
|
||||||
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
if (clean) parts.push(theme.fg("dim", clean));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Active Claude chat session
|
||||||
|
const chatRaw = statuses.get("chat-claude");
|
||||||
|
if (chatRaw) {
|
||||||
|
parts.push(theme.fg("accent", stripAnsi(chatRaw).trim()));
|
||||||
|
}
|
||||||
|
|
||||||
return parts.join(sep);
|
return parts.join(sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Event handlers
|
// Event handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
pi.on("session_start", async (_event, _ctx) => {
|
pi.on("session_start", (_event, _ctx) => {
|
||||||
ctx = _ctx;
|
ctx = _ctx;
|
||||||
installFooter(_ctx);
|
installFooter(_ctx);
|
||||||
});
|
});
|
||||||
@@ -230,11 +230,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
if (tuiRef) tuiRef.requestRender();
|
if (tuiRef) tuiRef.requestRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render when account switches (usage:update comes from usage-bars setStatus which
|
|
||||||
// already triggers a render, but account icon needs a nudge too)
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for usage updates — store raw values so we can build bars + dynamic
|
// Listen for usage updates — store raw values so we can build bars + dynamic
|
||||||
// countdown directly rather than parsing the ANSI status string from usage-bars.
|
// countdown directly rather than parsing the ANSI status string from usage-bars.
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* Footer Display Extension
|
|
||||||
*
|
|
||||||
* Replaces the built-in pi footer with a single clean line that assembles
|
|
||||||
* status from all other extensions:
|
|
||||||
*
|
|
||||||
* \uEF85 | S ⣿⣶⣀⣀⣀ 34% 2h 55m | W ⣿⣿⣷⣀⣀ 68% ⟳ Fri 09:00 | C ⣿⣀⣀⣀⣀ 20% | Sonnet 4.6 | rust-analyzer | MCP: 1/2
|
|
||||||
*
|
|
||||||
* Status sources:
|
|
||||||
* "claude-account" — set by claude-account-switch.ts → just the icon
|
|
||||||
* "usage-bars" — set by usage-bars extension → S/W bars (pass-through)
|
|
||||||
* ctx.getContextUsage() → C bar (rendered here)
|
|
||||||
* ctx.model → model short name
|
|
||||||
* "lsp" — set by lsp-pi extension → strip "LSP " prefix
|
|
||||||
* "mcp" — set by pi-mcp-adapter → strip " servers" suffix
|
|
||||||
*/
|
|
||||||
|
|
||||||
import os from "os";
|
|
||||||
import path from "path";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Braille gradient bar — used here only for the context (C) bar
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const BRAILLE_GRADIENT = "\u28C0\u28C4\u28E4\u28E6\u28F6\u28F7\u28FF";
|
|
||||||
const BRAILLE_EMPTY = "\u28C0";
|
|
||||||
const BAR_WIDTH = 5;
|
|
||||||
|
|
||||||
function renderBrailleBar(theme: any, value: number): string {
|
|
||||||
const v = Math.max(0, Math.min(100, Math.round(value)));
|
|
||||||
const levels = BRAILLE_GRADIENT.length - 1;
|
|
||||||
const totalSteps = BAR_WIDTH * levels;
|
|
||||||
const filledSteps = Math.round((v / 100) * totalSteps);
|
|
||||||
const full = Math.floor(filledSteps / levels);
|
|
||||||
const partial = filledSteps % levels;
|
|
||||||
const empty = BAR_WIDTH - full - (partial ? 1 : 0);
|
|
||||||
const color = v >= 90 ? "error" : v >= 70 ? "warning" : "success";
|
|
||||||
const filled = BRAILLE_GRADIENT[BRAILLE_GRADIENT.length - 1]!.repeat(Math.max(0, full));
|
|
||||||
const partialChar = partial ? BRAILLE_GRADIENT[partial]! : "";
|
|
||||||
const emptyChars = BRAILLE_EMPTY.repeat(Math.max(0, empty));
|
|
||||||
return theme.fg(color, filled + partialChar) + theme.fg("dim", emptyChars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function stripAnsi(text: string): string {
|
|
||||||
return text.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelShortName(modelId: string): string {
|
|
||||||
// claude-haiku-4-5 → "Haiku 4.5", claude-sonnet-4-6 → "Sonnet 4.6"
|
|
||||||
const m = modelId.match(/^claude-([a-z]+)-([\d]+(?:-[\d]+)*)(?:-\d{8})?$/);
|
|
||||||
if (m) {
|
|
||||||
const family = m[1]!.charAt(0).toUpperCase() + m[1]!.slice(1);
|
|
||||||
return `${family} ${m[2]!.replace(/-/g, ".")}`;
|
|
||||||
}
|
|
||||||
// claude-3-5-sonnet, claude-3-opus, etc.
|
|
||||||
const m2 = modelId.match(/^claude-[\d-]+-([a-z]+)/);
|
|
||||||
if (m2) return m2[1]!.charAt(0).toUpperCase() + m2[1]!.slice(1);
|
|
||||||
return modelId.replace(/^claude-/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nerd Font codepoints matched to what claude-account-switch.ts emits
|
|
||||||
const ICON_PERSONAL = "\uEF85"; // U+EF85 — home
|
|
||||||
const ICON_WORK = "\uDB80\uDCD6"; // U+F00D6 — briefcase (surrogate pair)
|
|
||||||
const ICON_UNKNOWN = "\uF420"; // U+F420 — claude default
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
let ctx: any = null;
|
|
||||||
let tuiRef: any = null;
|
|
||||||
let footerDataRef: any = null;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Footer line builder — called on every render
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function buildFooterLine(theme: any): string {
|
|
||||||
const sep = theme.fg("dim", " · ");
|
|
||||||
const pipeSep = theme.fg("dim", " | ");
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
const statuses: ReadonlyMap<string, string> =
|
|
||||||
footerDataRef?.getExtensionStatuses?.() ?? new Map();
|
|
||||||
|
|
||||||
// 1. Current working directory
|
|
||||||
const home = os.homedir();
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const dir = cwd.startsWith(home)
|
|
||||||
? "~" + path.sep + path.relative(home, cwd)
|
|
||||||
: cwd;
|
|
||||||
parts.push(theme.fg("dim", dir));
|
|
||||||
|
|
||||||
// 2. Account icon
|
|
||||||
const acctRaw = statuses.get("claude-account");
|
|
||||||
if (acctRaw !== undefined) {
|
|
||||||
const clean = stripAnsi(acctRaw).trim();
|
|
||||||
let icon: string;
|
|
||||||
if (clean.includes("personal")) icon = ICON_PERSONAL;
|
|
||||||
else if (clean.includes("work")) icon = ICON_WORK;
|
|
||||||
else icon = ICON_UNKNOWN;
|
|
||||||
parts.push(theme.fg("dim", icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. S / W usage bars + C bar — joined as one |-separated block
|
|
||||||
const usageRaw = statuses.get("usage-bars");
|
|
||||||
const contextUsage = ctx?.getContextUsage?.();
|
|
||||||
{
|
|
||||||
let block = usageRaw ?? "";
|
|
||||||
if (contextUsage && contextUsage.percent !== null) {
|
|
||||||
const pct = Math.round(contextUsage.percent);
|
|
||||||
const cBar =
|
|
||||||
theme.fg("muted", "C ") +
|
|
||||||
renderBrailleBar(theme, pct) +
|
|
||||||
" " +
|
|
||||||
theme.fg("dim", `${pct}%`);
|
|
||||||
block = block ? block + pipeSep + cBar : cBar;
|
|
||||||
}
|
|
||||||
if (block) parts.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Model short name
|
|
||||||
const modelId = ctx?.model?.id;
|
|
||||||
if (modelId) parts.push(theme.fg("dim", getModelShortName(modelId)));
|
|
||||||
|
|
||||||
// 5. LSP — strip "LSP" prefix and activity dot
|
|
||||||
const lspRaw = statuses.get("lsp");
|
|
||||||
if (lspRaw) {
|
|
||||||
const clean = stripAnsi(lspRaw).trim().replace(/^LSP\s*[•·]?\s*/i, "").trim();
|
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. MCP — strip " servers" suffix
|
|
||||||
const mcpRaw = statuses.get("mcp");
|
|
||||||
if (mcpRaw) {
|
|
||||||
const clean = stripAnsi(mcpRaw).trim().replace(/\s*servers?.*$/, "").trim();
|
|
||||||
if (clean) parts.push(theme.fg("dim", clean));
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(sep);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Footer installation
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
function installFooter(_ctx: any) {
|
|
||||||
if (!_ctx?.hasUI) return;
|
|
||||||
_ctx.ui.setFooter((_tui: any, theme: any, footerData: any) => {
|
|
||||||
tuiRef = _tui;
|
|
||||||
footerDataRef = footerData;
|
|
||||||
const unsub = footerData.onBranchChange(() => _tui.requestRender());
|
|
||||||
return {
|
|
||||||
dispose: unsub,
|
|
||||||
invalidate() {},
|
|
||||||
render(width: number): string[] {
|
|
||||||
return [truncateToWidth(buildFooterLine(theme) || "", width)];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Event handlers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
pi.on("session_start", async (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
installFooter(_ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
pi.on("session_shutdown", (_event, _ctx) => {
|
|
||||||
if (_ctx?.hasUI) _ctx.ui.setFooter(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render after turns so context usage stays current
|
|
||||||
pi.on("turn_end", (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render when model changes (updates model name in footer)
|
|
||||||
pi.on("model_select", (_event, _ctx) => {
|
|
||||||
ctx = _ctx;
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render when account switches (usage:update comes from usage-bars setStatus which
|
|
||||||
// already triggers a render, but account icon needs a nudge too)
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
if (tuiRef) tuiRef.requestRender();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Local Explorer Extension
|
|
||||||
*
|
|
||||||
* Previously auto-injected a scout hint into every cloud-model session.
|
|
||||||
* Now a no-op — scout delegation is opt-in via the `local-scout` skill.
|
|
||||||
* Invoke with /skill:local-scout or by mentioning "use scout" in a prompt.
|
|
||||||
*
|
|
||||||
* Placed in: ~/.pi/agent/extensions/local-explorer.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
|
|
||||||
export default function (_pi: ExtensionAPI) {
|
|
||||||
// No automatic injection — scout is opt-in via the local-scout skill.
|
|
||||||
}
|
|
||||||
56
pi/.pi/agent/extensions/pi-ask-mcp/README.md
Normal file
56
pi/.pi/agent/extensions/pi-ask-mcp/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# pi-ask-mcp
|
||||||
|
|
||||||
|
A minimal MCP stdio server that gives Claude **one** tool — `ask` — which routes
|
||||||
|
structured questions back to pi's native ask UI instead of using Claude's
|
||||||
|
built-in `AskUserQuestion`.
|
||||||
|
|
||||||
|
This is **not** a regular pi extension. It is a subprocess of `claude`, which is
|
||||||
|
itself a subprocess of the `chat-claude` extension. The pi-side counterpart is
|
||||||
|
[`shared/pi-ask-bridge.ts`](../../shared/pi-ask-bridge.ts), which:
|
||||||
|
|
||||||
|
1. Opens a Unix-domain socket per chat session.
|
||||||
|
2. Generates an `--mcp-config` JSON pointing here, with `PI_ASK_SOCKET=<sock>`.
|
||||||
|
3. Translates `ask` requests off the socket into
|
||||||
|
`askSingleQuestionWithInlineNote` / `askQuestionsWithTabs` calls and writes
|
||||||
|
the result back.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
pi
|
||||||
|
└── chat-claude
|
||||||
|
├── pi-ask-bridge (UDS server, owns ui.custom)
|
||||||
|
└── claude -p ... --mcp-config <generated.json> --disallowed-tools AskUserQuestion
|
||||||
|
└── pi-ask-mcp/server.js (this file)
|
||||||
|
↳ on tools/call ask → connect $PI_ASK_SOCKET → ask → reply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why a hand-written MCP server
|
||||||
|
|
||||||
|
No `@modelcontextprotocol/sdk` dependency, no transpile step, no
|
||||||
|
`node_modules`. The MCP stdio protocol is small enough (~6 method handlers)
|
||||||
|
that writing it directly keeps the file self-contained and trivially
|
||||||
|
portable. Claude CLI spawns it via `node server.js`.
|
||||||
|
|
||||||
|
## Wire format
|
||||||
|
|
||||||
|
Stdio (with Claude): JSON-RPC 2.0 over newline-delimited JSON.
|
||||||
|
|
||||||
|
Socket (with pi-ask-bridge): NDJSON, one request → one response, then close.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// → pi
|
||||||
|
{ "id": "uuid", "type": "ask",
|
||||||
|
"questions": [
|
||||||
|
{ "id": "auth", "question": "Auth method?",
|
||||||
|
"options": [{"label": "OAuth"}, {"label": "API key"}],
|
||||||
|
"multi": false, "recommended": 0 }
|
||||||
|
] }
|
||||||
|
|
||||||
|
// ← pi (success)
|
||||||
|
{ "id": "uuid", "type": "result",
|
||||||
|
"results": [{ "id": "auth", "selectedOptions": ["OAuth"] }] }
|
||||||
|
|
||||||
|
// ← pi (cancel / error)
|
||||||
|
{ "id": "uuid", "type": "error", "message": "cancelled" }
|
||||||
|
```
|
||||||
7
pi/.pi/agent/extensions/pi-ask-mcp/package.json
Normal file
7
pi/.pi/agent/extensions/pi-ask-mcp/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-ask-mcp",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "server.js",
|
||||||
|
"description": "Minimal MCP stdio server bridging Claude → pi-ask-bridge."
|
||||||
|
}
|
||||||
195
pi/.pi/agent/extensions/pi-ask-mcp/server.js
Executable file
195
pi/.pi/agent/extensions/pi-ask-mcp/server.js
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// pi-ask-mcp/server.js
|
||||||
|
//
|
||||||
|
// Minimal MCP stdio server that exposes ONE tool: `ask`.
|
||||||
|
// Bridges Claude → pi via a Unix-domain socket: when Claude calls the tool,
|
||||||
|
// this server forwards the question(s) to pi over $PI_ASK_SOCKET, awaits
|
||||||
|
// the user's answer, and returns it as the tool result.
|
||||||
|
//
|
||||||
|
// Wire format with Claude (stdin/stdout): JSON-RPC 2.0 over NDJSON.
|
||||||
|
// Wire format with pi (PI_ASK_SOCKET): NDJSON request/response, see
|
||||||
|
// ../../shared/pi-ask-bridge.ts.
|
||||||
|
//
|
||||||
|
// This file is INTENTIONALLY plain JavaScript (no transpile step, no
|
||||||
|
// node_modules) — Claude CLI spawns it via `node <path>`. Keep it small,
|
||||||
|
// dependency-free, and self-contained.
|
||||||
|
|
||||||
|
import { connect } from "node:net";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { createInterface } from "node:readline";
|
||||||
|
|
||||||
|
// ── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
const SOCKET = process.env.PI_ASK_SOCKET;
|
||||||
|
if (!SOCKET) {
|
||||||
|
process.stderr.write("[pi-ask-mcp] PI_ASK_SOCKET env var is required\n");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_INFO = { name: "pi", version: "0.1.0" };
|
||||||
|
const PROTOCOL_VERSION = "2024-11-05";
|
||||||
|
const SOCKET_TIMEOUT_MS = 15 * 60 * 1000; // matches runClaude's default
|
||||||
|
|
||||||
|
// ── Tool schema (kept in sync with pi-ask-tool/index.ts AskParamsSchema) ──
|
||||||
|
const ASK_INPUT_SCHEMA = {
|
||||||
|
type: "object",
|
||||||
|
required: ["questions"],
|
||||||
|
properties: {
|
||||||
|
questions: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
description: "One or more questions to ask the user.",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id", "question", "options"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "Stable id (e.g. 'auth', 'cache')." },
|
||||||
|
question: { type: "string", description: "Question text shown to the user." },
|
||||||
|
options: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
description: "2-5 concise options. Do NOT include 'Other' (UI adds it).",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
required: ["label"],
|
||||||
|
properties: {
|
||||||
|
label: { type: "string", description: "Option display label." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
multi: { type: "boolean", description: "Allow multi-select. Defaults to false." },
|
||||||
|
recommended: { type: "number", description: "0-indexed recommended option (default highlight)." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ASK_DESCRIPTION = [
|
||||||
|
"Ask the user one or more structured questions through pi's native TUI.",
|
||||||
|
"Use this whenever a choice materially affects the outcome — instead of",
|
||||||
|
"guessing or the built-in AskUserQuestion. Provide 2-5 concise options.",
|
||||||
|
"Set multi=true when multiple answers are valid. Do NOT include an 'Other'",
|
||||||
|
"option (UI adds it automatically). The result is a JSON array of",
|
||||||
|
"{id, selectedOptions[], customInput?} per question — empty selectedOptions",
|
||||||
|
"means the user cancelled.",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
// ── stdio framing: NDJSON ──────────────────────────────────────────────────
|
||||||
|
const rl = createInterface({ input: process.stdin });
|
||||||
|
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
||||||
|
const log = (msg) => process.stderr.write(`[pi-ask-mcp] ${msg}\n`);
|
||||||
|
|
||||||
|
// ── socket round-trip to pi-ask-bridge ─────────────────────────────────────
|
||||||
|
function askPi(args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sock = connect(SOCKET);
|
||||||
|
const id = randomUUID();
|
||||||
|
let buf = "";
|
||||||
|
let settled = false;
|
||||||
|
const finish = (fn, val) => { if (settled) return; settled = true; clearTimeout(t); fn(val); try { sock.end(); } catch {} };
|
||||||
|
const t = setTimeout(
|
||||||
|
() => finish(reject, new Error(`pi-ask bridge timeout after ${SOCKET_TIMEOUT_MS / 1000}s`)),
|
||||||
|
SOCKET_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
sock.on("connect", () => sock.write(JSON.stringify({ id, type: "ask", ...args }) + "\n"));
|
||||||
|
sock.on("data", (d) => {
|
||||||
|
buf += d.toString();
|
||||||
|
const nl = buf.indexOf("\n");
|
||||||
|
if (nl < 0) return;
|
||||||
|
try { finish(resolve, JSON.parse(buf.slice(0, nl))); }
|
||||||
|
catch (err) { finish(reject, err); }
|
||||||
|
});
|
||||||
|
sock.on("error", (err) => finish(reject, err));
|
||||||
|
sock.on("close", () => {
|
||||||
|
if (!settled) finish(reject, new Error("pi-ask bridge closed connection without reply"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON-RPC method handlers ───────────────────────────────────────────────
|
||||||
|
async function handleRequest(req) {
|
||||||
|
const { id, method, params } = req;
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case "initialize":
|
||||||
|
return ok(id, {
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo: SERVER_INFO,
|
||||||
|
});
|
||||||
|
case "tools/list":
|
||||||
|
return ok(id, {
|
||||||
|
tools: [{ name: "ask", description: ASK_DESCRIPTION, inputSchema: ASK_INPUT_SCHEMA }],
|
||||||
|
});
|
||||||
|
case "tools/call": {
|
||||||
|
const name = params?.name;
|
||||||
|
const args = params?.arguments ?? {};
|
||||||
|
if (name !== "ask") return err(id, -32602, `unknown tool: ${name}`);
|
||||||
|
const reply = await askPi(args);
|
||||||
|
if (reply.type === "error") {
|
||||||
|
return ok(id, {
|
||||||
|
isError: true,
|
||||||
|
content: [{ type: "text", text: `(user did not answer: ${reply.message})` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ok(id, {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(reply.results, null, 2) }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "ping": return ok(id, {});
|
||||||
|
case "resources/list": return ok(id, { resources: [] });
|
||||||
|
case "prompts/list": return ok(id, { prompts: [] });
|
||||||
|
default: return err(id, -32601, `method not found: ${method}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return err(id, -32603, e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = (id, result) => ({ jsonrpc: "2.0", id, result });
|
||||||
|
const err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
|
||||||
|
|
||||||
|
// ── main loop ──────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Track in-flight handlers so we don't exit before they finish. Without this,
|
||||||
|
// `node server.js <<<input` (or any case where stdin closes mid-request) would
|
||||||
|
// race the async tools/call handler against rl 'close' → process.exit, and
|
||||||
|
// the reply would silently disappear.
|
||||||
|
let inflight = 0;
|
||||||
|
let stdinClosed = false;
|
||||||
|
function drainAndExit(code = 0) {
|
||||||
|
if (inflight === 0) process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOne(msg) {
|
||||||
|
// Notifications carry no id and expect no response.
|
||||||
|
if (msg.id === undefined || msg.id === null) {
|
||||||
|
if (msg.method === "exit") drainAndExit(0);
|
||||||
|
return; // notifications/initialized, notifications/cancelled, etc.
|
||||||
|
}
|
||||||
|
inflight += 1;
|
||||||
|
try {
|
||||||
|
const reply = await handleRequest(msg);
|
||||||
|
if (reply) send(reply);
|
||||||
|
} finally {
|
||||||
|
inflight -= 1;
|
||||||
|
if (stdinClosed) drainAndExit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let msg;
|
||||||
|
try { msg = JSON.parse(line); } catch { return; }
|
||||||
|
if (Array.isArray(msg)) {
|
||||||
|
for (const m of msg) void handleOne(m);
|
||||||
|
} else {
|
||||||
|
void handleOne(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("close", () => { stdinClosed = true; drainAndExit(0); });
|
||||||
|
process.on("SIGTERM", () => process.exit(0));
|
||||||
|
process.on("SIGINT", () => process.exit(0));
|
||||||
|
log("ready, socket=" + SOCKET);
|
||||||
23
pi/.pi/agent/extensions/pi-ask-tool/README.md
Normal file
23
pi/.pi/agent/extensions/pi-ask-tool/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Pi Ask Tool Extension
|
||||||
|
|
||||||
|
This extension bridges Claude Code's ask functionality into pi's TUI, allowing users to ask questions and receive answers directly in the TUI interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Seamless integration with pi's TUI
|
||||||
|
- Support for all Claude agents (plan_review, code_review, debug, oracle)
|
||||||
|
- Multi-turn conversations with session management
|
||||||
|
- Context-aware responses based on codebase exploration
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Run the CLI agent: `pi-ask-tool`
|
||||||
|
2. Type your question in the TUI
|
||||||
|
3. Receive answers directly in the TUI interface
|
||||||
|
4. Continue the conversation or start new ones
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Default agent: `code_review`
|
||||||
|
Default model: `sonnet`
|
||||||
|
Session persistence: Enabled
|
||||||
37
pi/.pi/agent/extensions/pi-ask-tool/cli.ts
Normal file
37
pi/.pi/agent/extensions/pi-ask-tool/cli.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Pi Ask Tool CLI Agent
|
||||||
|
|
||||||
|
import { ask_claude } from "../../@piplugin/ask-claude"
|
||||||
|
import { sessionId } from '../shared';
|
||||||
|
|
||||||
|
export async function start() {
|
||||||
|
console.log('Pi Ask Tool initialized');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Get user input from TUI (simplified for example)
|
||||||
|
const userInput = await getTUIInput('Ask a question:');
|
||||||
|
|
||||||
|
// Handle multi-turn sessions via sessionId
|
||||||
|
const response = await ask_claude({
|
||||||
|
prompt: userInput,
|
||||||
|
agent: 'code_review', // Default agent
|
||||||
|
session_id: sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display answer in TUI
|
||||||
|
await showTUIResult(response);
|
||||||
|
|
||||||
|
// Update session context if needed
|
||||||
|
sessionId = response.session_id || sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock TUI handlers - implement actual TUI integration
|
||||||
|
async function getTUIInput(question: string) {
|
||||||
|
// Replace with real TUI input method
|
||||||
|
const input = process.stdin.read().toString();
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showTUIResult(result: any) {
|
||||||
|
console.log('Answer:', result.summary || result);
|
||||||
|
}
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
/**
|
|
||||||
* Qwen Provider Extension
|
|
||||||
*
|
|
||||||
* Registers Qwen 3.5 models via the qwen.ai OAuth flow (chat.qwen.ai).
|
|
||||||
* Based on the upstream custom-provider-qwen-cli example.
|
|
||||||
*
|
|
||||||
* Models:
|
|
||||||
* - qwen3.5-plus (Qwen3.5 best — rivals Qwen3-Max, 1M ctx, cheaper)
|
|
||||||
* - qwen3.5-flash (Qwen3.5 fast & cheap, 1M ctx)
|
|
||||||
* - qwen3-max (Qwen3 flagship, strongest reasoning, 262K ctx)
|
|
||||||
* - qwen-plus (Qwen3 balanced, 1M ctx)
|
|
||||||
* - qwen-flash (Qwen3 fast, 1M ctx)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* /login qwen-cli (browser OAuth)
|
|
||||||
* or set QWEN_CLI_API_KEY=...
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Constants
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const QWEN_DEVICE_CODE_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/device/code";
|
|
||||||
const QWEN_TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token";
|
|
||||||
const QWEN_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
||||||
const QWEN_SCOPE = "openid profile email model.completion";
|
|
||||||
const QWEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
||||||
const QWEN_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
||||||
const QWEN_POLL_INTERVAL_MS = 2000;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// PKCE Helpers
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
|
|
||||||
const array = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(array);
|
|
||||||
const verifier = btoa(String.fromCharCode(...array))
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const data = encoder.encode(verifier);
|
|
||||||
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
||||||
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
|
|
||||||
return { verifier, challenge };
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// OAuth Implementation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface DeviceCodeResponse {
|
|
||||||
device_code: string;
|
|
||||||
user_code: string;
|
|
||||||
verification_uri: string;
|
|
||||||
verification_uri_complete?: string;
|
|
||||||
expires_in: number;
|
|
||||||
interval?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenResponse {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token?: string;
|
|
||||||
token_type: string;
|
|
||||||
expires_in: number;
|
|
||||||
resource_url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
reject(new Error("Login cancelled"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timeout = setTimeout(resolve, ms);
|
|
||||||
signal?.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(new Error("Login cancelled"));
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startDeviceFlow(): Promise<{ deviceCode: DeviceCodeResponse; verifier: string }> {
|
|
||||||
const { verifier, challenge } = await generatePKCE();
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
scope: QWEN_SCOPE,
|
|
||||||
code_challenge: challenge,
|
|
||||||
code_challenge_method: "S256",
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
};
|
|
||||||
const requestId = globalThis.crypto?.randomUUID?.();
|
|
||||||
if (requestId) headers["x-request-id"] = requestId;
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_DEVICE_CODE_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Device code request failed: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as DeviceCodeResponse;
|
|
||||||
|
|
||||||
if (!data.device_code || !data.user_code || !data.verification_uri) {
|
|
||||||
throw new Error("Invalid device code response: missing required fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { deviceCode: data, verifier };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollForToken(
|
|
||||||
deviceCode: string,
|
|
||||||
verifier: string,
|
|
||||||
intervalSeconds: number | undefined,
|
|
||||||
expiresIn: number,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<TokenResponse> {
|
|
||||||
const deadline = Date.now() + expiresIn * 1000;
|
|
||||||
const resolvedIntervalSeconds =
|
|
||||||
typeof intervalSeconds === "number" && Number.isFinite(intervalSeconds) && intervalSeconds > 0
|
|
||||||
? intervalSeconds
|
|
||||||
: QWEN_POLL_INTERVAL_MS / 1000;
|
|
||||||
let intervalMs = Math.max(1000, Math.floor(resolvedIntervalSeconds * 1000));
|
|
||||||
|
|
||||||
const handleTokenError = async (error: string, description?: string): Promise<boolean> => {
|
|
||||||
switch (error) {
|
|
||||||
case "authorization_pending":
|
|
||||||
await abortableSleep(intervalMs, signal);
|
|
||||||
return true;
|
|
||||||
case "slow_down":
|
|
||||||
intervalMs = Math.min(intervalMs + 5000, 10000);
|
|
||||||
await abortableSleep(intervalMs, signal);
|
|
||||||
return true;
|
|
||||||
case "expired_token":
|
|
||||||
throw new Error("Device code expired. Please restart authentication.");
|
|
||||||
case "access_denied":
|
|
||||||
throw new Error("Authorization denied by user.");
|
|
||||||
default:
|
|
||||||
throw new Error(`Token request failed: ${error} - ${description || ""}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
if (signal?.aborted) throw new Error("Login cancelled");
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
grant_type: QWEN_GRANT_TYPE,
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
device_code: deviceCode,
|
|
||||||
code_verifier: verifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_TOKEN_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
let data: (TokenResponse & { error?: string; error_description?: string }) | null = null;
|
|
||||||
if (responseText) {
|
|
||||||
try {
|
|
||||||
data = JSON.parse(responseText) as TokenResponse & { error?: string; error_description?: string };
|
|
||||||
} catch {
|
|
||||||
data = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = data?.error;
|
|
||||||
const errorDescription = data?.error_description;
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (error && (await handleTokenError(error, errorDescription))) continue;
|
|
||||||
throw new Error(`Token request failed: ${response.status} ${response.statusText}. Response: ${responseText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.access_token) return data;
|
|
||||||
|
|
||||||
if (error && (await handleTokenError(error, errorDescription))) continue;
|
|
||||||
|
|
||||||
throw new Error("Token request failed: missing access token in response");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Authentication timed out. Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginQwen(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
||||||
const { deviceCode, verifier } = await startDeviceFlow();
|
|
||||||
const authUrl = deviceCode.verification_uri_complete || deviceCode.verification_uri;
|
|
||||||
const instructions = deviceCode.verification_uri_complete
|
|
||||||
? undefined
|
|
||||||
: `Enter code: ${deviceCode.user_code}`;
|
|
||||||
callbacks.onAuth({ url: authUrl, instructions });
|
|
||||||
|
|
||||||
const tokenResponse = await pollForToken(
|
|
||||||
deviceCode.device_code,
|
|
||||||
verifier,
|
|
||||||
deviceCode.interval,
|
|
||||||
deviceCode.expires_in,
|
|
||||||
callbacks.signal,
|
|
||||||
);
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + tokenResponse.expires_in * 1000 - 5 * 60 * 1000;
|
|
||||||
|
|
||||||
return {
|
|
||||||
refresh: tokenResponse.refresh_token || "",
|
|
||||||
access: tokenResponse.access_token,
|
|
||||||
expires: expiresAt,
|
|
||||||
enterpriseUrl: tokenResponse.resource_url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshQwenToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token: credentials.refresh,
|
|
||||||
client_id: QWEN_CLIENT_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(QWEN_TOKEN_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: body.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Token refresh failed: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as TokenResponse;
|
|
||||||
if (!data.access_token) throw new Error("Token refresh failed: no access token in response");
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + data.expires_in * 1000 - 5 * 60 * 1000;
|
|
||||||
|
|
||||||
return {
|
|
||||||
refresh: data.refresh_token || credentials.refresh,
|
|
||||||
access: data.access_token,
|
|
||||||
expires: expiresAt,
|
|
||||||
enterpriseUrl: data.resource_url ?? credentials.enterpriseUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQwenBaseUrl(resourceUrl?: string): string {
|
|
||||||
if (!resourceUrl) return QWEN_DEFAULT_BASE_URL;
|
|
||||||
let url = resourceUrl.startsWith("http") ? resourceUrl : `https://${resourceUrl}`;
|
|
||||||
if (!url.endsWith("/v1")) url = `${url}/v1`;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Extension Entry Point
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
|
||||||
pi.registerProvider("qwen-cli", {
|
|
||||||
baseUrl: QWEN_DEFAULT_BASE_URL,
|
|
||||||
apiKey: "QWEN_CLI_API_KEY",
|
|
||||||
api: "openai-completions",
|
|
||||||
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "qwen3.5-plus",
|
|
||||||
name: "Qwen 3.5 Plus (Best — rivals Qwen3-Max)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 65536,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen3.5-flash",
|
|
||||||
name: "Qwen 3.5 Flash (Fast & Cheap)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 65536,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen3-max",
|
|
||||||
name: "Qwen 3 Max (Flagship, strongest reasoning)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 262144,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen-plus",
|
|
||||||
name: "Qwen 3 Plus (Balanced)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen-flash",
|
|
||||||
name: "Qwen 3 Flash (Fast)",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000000,
|
|
||||||
maxTokens: 32768,
|
|
||||||
compat: {
|
|
||||||
supportsDeveloperRole: false,
|
|
||||||
supportsReasoningEffort: false,
|
|
||||||
maxTokensField: "max_tokens",
|
|
||||||
thinkingFormat: "qwen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
oauth: {
|
|
||||||
name: "Qwen CLI",
|
|
||||||
login: loginQwen,
|
|
||||||
refreshToken: refreshQwenToken,
|
|
||||||
getApiKey: (cred) => cred.access,
|
|
||||||
modifyModels: (models, cred) => {
|
|
||||||
const baseUrl = getQwenBaseUrl(cred.enterpriseUrl as string | undefined);
|
|
||||||
return models.map((m) => (m.provider === "qwen-cli" ? { ...m, baseUrl } : m));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -55,6 +55,8 @@ export interface UsageData {
|
|||||||
/** Unix ms timestamp of when the session window resets (from the raw API response). */
|
/** Unix ms timestamp of when the session window resets (from the raw API response). */
|
||||||
sessionResetsAt?: number;
|
sessionResetsAt?: number;
|
||||||
weeklyResetsIn?: string;
|
weeklyResetsIn?: string;
|
||||||
|
/** Unix ms timestamp of when the weekly window resets. */
|
||||||
|
weeklyResetsAt?: number;
|
||||||
extraSpend?: number;
|
extraSpend?: number;
|
||||||
extraLimit?: number;
|
extraLimit?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -321,13 +323,34 @@ export function formatResetsAt(isoDate: string, nowMs = Date.now()): string {
|
|||||||
return formatDuration(diffSeconds);
|
return formatDuration(diffSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CLAUDE_CREDENTIALS_FILE = path.join(os.homedir(), ".claude", ".credentials.json");
|
||||||
|
|
||||||
export function readAuth(authFile = DEFAULT_AUTH_FILE): AuthData | null {
|
export function readAuth(authFile = DEFAULT_AUTH_FILE): AuthData | null {
|
||||||
|
let result: AuthData | null = null;
|
||||||
|
|
||||||
|
// Read pi auth.json for non-Claude providers
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
const parsed = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
||||||
return asObject(parsed) as AuthData;
|
result = asObject(parsed) as AuthData;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
result = {} as AuthData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read Claude credentials from ~/.claude/.credentials.json
|
||||||
|
try {
|
||||||
|
const claudeRaw = fs.readFileSync(CLAUDE_CREDENTIALS_FILE, "utf-8");
|
||||||
|
const claudeCreds = JSON.parse(claudeRaw);
|
||||||
|
const oauth = claudeCreds?.claudeAiOauth;
|
||||||
|
if (oauth?.accessToken) {
|
||||||
|
result!.anthropic = {
|
||||||
|
access: oauth.accessToken,
|
||||||
|
refresh: oauth.refreshToken,
|
||||||
|
expires: typeof oauth.expiresAt === "number" ? oauth.expiresAt : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeAuth(auth: AuthData, authFile = DEFAULT_AUTH_FILE): boolean {
|
export function writeAuth(auth: AuthData, authFile = DEFAULT_AUTH_FILE): boolean {
|
||||||
@@ -647,6 +670,9 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
|
|||||||
const sessionResetsAt = data?.five_hour?.resets_at
|
const sessionResetsAt = data?.five_hour?.resets_at
|
||||||
? new Date(data.five_hour.resets_at).getTime()
|
? new Date(data.five_hour.resets_at).getTime()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const weeklyResetsAt = data?.seven_day?.resets_at
|
||||||
|
? new Date(data.seven_day.resets_at).getTime()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const usage: UsageData = {
|
const usage: UsageData = {
|
||||||
session: readPercentCandidate(data?.five_hour?.utilization) ?? 0,
|
session: readPercentCandidate(data?.five_hour?.utilization) ?? 0,
|
||||||
@@ -654,6 +680,7 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
|
|||||||
sessionResetsIn: data?.five_hour?.resets_at ? formatResetsAt(data.five_hour.resets_at) : undefined,
|
sessionResetsIn: data?.five_hour?.resets_at ? formatResetsAt(data.five_hour.resets_at) : undefined,
|
||||||
sessionResetsAt: Number.isFinite(sessionResetsAt) ? sessionResetsAt : undefined,
|
sessionResetsAt: Number.isFinite(sessionResetsAt) ? sessionResetsAt : undefined,
|
||||||
weeklyResetsIn: data?.seven_day?.resets_at ? formatResetsAt(data.seven_day.resets_at) : undefined,
|
weeklyResetsIn: data?.seven_day?.resets_at ? formatResetsAt(data.seven_day.resets_at) : undefined,
|
||||||
|
weeklyResetsAt: Number.isFinite(weeklyResetsAt) ? weeklyResetsAt : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data?.extra_usage?.is_enabled) {
|
if (data?.extra_usage?.is_enabled) {
|
||||||
|
|||||||
@@ -306,14 +306,27 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const active = state.activeProvider;
|
const active = state.activeProvider;
|
||||||
const data = active ? state[active] : null;
|
const data = active ? state[active] : null;
|
||||||
|
|
||||||
// Always emit event for other extensions (e.g. footer-display)
|
// Always emit Claude usage for other extensions (e.g. footer-display)
|
||||||
if (data && !data.error) {
|
// so S/W bars are visible regardless of active model.
|
||||||
|
const claudeData = state.claude;
|
||||||
|
if (claudeData && !claudeData.error) {
|
||||||
|
pi.events.emit("usage:update", {
|
||||||
|
session: claudeData.session,
|
||||||
|
weekly: claudeData.weekly,
|
||||||
|
sessionResetsIn: claudeData.sessionResetsIn,
|
||||||
|
sessionResetsAt: claudeData.sessionResetsAt,
|
||||||
|
weeklyResetsIn: claudeData.weeklyResetsIn,
|
||||||
|
weeklyResetsAt: claudeData.weeklyResetsAt,
|
||||||
|
});
|
||||||
|
} else if (data && !data.error) {
|
||||||
|
// Fallback to active provider data if Claude data unavailable
|
||||||
pi.events.emit("usage:update", {
|
pi.events.emit("usage:update", {
|
||||||
session: data.session,
|
session: data.session,
|
||||||
weekly: data.weekly,
|
weekly: data.weekly,
|
||||||
sessionResetsIn: data.sessionResetsIn,
|
sessionResetsIn: data.sessionResetsIn,
|
||||||
sessionResetsAt: data.sessionResetsAt,
|
sessionResetsAt: data.sessionResetsAt,
|
||||||
weeklyResetsIn: data.weeklyResetsIn,
|
weeklyResetsIn: data.weeklyResetsIn,
|
||||||
|
weeklyResetsAt: data.weeklyResetsAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +385,48 @@ export default function (pi: ExtensionAPI) {
|
|||||||
const auth = readAuth();
|
const auth = readAuth();
|
||||||
const active = state.activeProvider;
|
const active = state.activeProvider;
|
||||||
|
|
||||||
|
// Always try to fetch Claude data so S/W bars show regardless of active provider
|
||||||
|
if (auth && canShowForProvider("claude", auth, endpoints)) {
|
||||||
|
try {
|
||||||
|
const cache = readUsageCache();
|
||||||
|
const now = Date.now();
|
||||||
|
const cacheTtl = options.cacheTtl ?? CACHE_TTL_MS;
|
||||||
|
const claudeBlockedUntil = cache?.rateLimitedUntil?.claude ?? 0;
|
||||||
|
if (now < claudeBlockedUntil) {
|
||||||
|
if (cache?.data?.claude) state.claude = cache.data.claude;
|
||||||
|
} else {
|
||||||
|
const claudeCacheFresh = cache && now - cache.timestamp < cacheTtl && cache.data?.claude;
|
||||||
|
if (claudeCacheFresh && !options.forceFresh) {
|
||||||
|
state.claude = cache.data.claude;
|
||||||
|
} else {
|
||||||
|
const claudeAccess = auth.anthropic?.access;
|
||||||
|
if (claudeAccess) {
|
||||||
|
const claudeResult = await fetchClaudeUsage(claudeAccess);
|
||||||
|
state.claude = claudeResult;
|
||||||
|
if (!claudeResult.error) {
|
||||||
|
const nextCache: import("./core").UsageCache = {
|
||||||
|
timestamp: now,
|
||||||
|
data: { ...(cache?.data ?? {}), claude: claudeResult },
|
||||||
|
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}) },
|
||||||
|
};
|
||||||
|
delete nextCache.rateLimitedUntil!.claude;
|
||||||
|
writeUsageCache(nextCache);
|
||||||
|
} else if (claudeResult.error === "HTTP 429") {
|
||||||
|
// Record backoff even when Claude is not the active provider —
|
||||||
|
// without this the prefetch would hammer the API on every poll.
|
||||||
|
const nextCache: import("./core").UsageCache = {
|
||||||
|
timestamp: cache?.timestamp ?? now,
|
||||||
|
data: { ...(cache?.data ?? {}) },
|
||||||
|
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}), claude: now + RATE_LIMITED_BACKOFF_MS },
|
||||||
|
};
|
||||||
|
writeUsageCache(nextCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!canShowForProvider(active, auth, endpoints) || !auth || !active) {
|
if (!canShowForProvider(active, auth, endpoints) || !auth || !active) {
|
||||||
state.lastPoll = Date.now(); updateStatus(); return;
|
state.lastPoll = Date.now(); updateStatus(); return;
|
||||||
}
|
}
|
||||||
@@ -478,7 +533,9 @@ export default function (pi: ExtensionAPI) {
|
|||||||
await Promise.race([runPollInner(options), timeout]);
|
await Promise.race([runPollInner(options), timeout]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLL_TIMEOUT_MS = 30_000;
|
// Must be less than the 25 000 ms timeout inside runPoll so the guard fires
|
||||||
|
// before runPoll's finally-block clears pollInFlight.
|
||||||
|
const POLL_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
async function poll(options: PollOptions = {}) {
|
async function poll(options: PollOptions = {}) {
|
||||||
// If a previous poll has been running longer than POLL_TIMEOUT_MS, abandon it
|
// If a previous poll has been running longer than POLL_TIMEOUT_MS, abandon it
|
||||||
@@ -557,15 +614,6 @@ export default function (pi: ExtensionAPI) {
|
|||||||
await poll({ cacheTtl: ACTIVE_CACHE_TTL_MS });
|
await poll({ cacheTtl: ACTIVE_CACHE_TTL_MS });
|
||||||
});
|
});
|
||||||
|
|
||||||
pi.events.on("claude-account:switched", () => {
|
|
||||||
const cache = readUsageCache();
|
|
||||||
if (cache?.data?.claude) {
|
|
||||||
const nextCache: import("./core").UsageCache = { ...cache, data: { ...cache.data } };
|
|
||||||
delete nextCache.data.claude;
|
|
||||||
writeUsageCache(nextCache);
|
|
||||||
}
|
|
||||||
void poll({ forceFresh: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for OpenCode Go spend events from other extensions
|
// Listen for OpenCode Go spend events from other extensions
|
||||||
pi.events.on("opencode-go:spend", async (amount: number) => {
|
pi.events.on("opencode-go:spend", async (amount: number) => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
@@ -101,6 +101,7 @@ local mock_wezterm = {
|
|||||||
log_warn = function() end,
|
log_warn = function() end,
|
||||||
log_error = function() end,
|
log_error = function() end,
|
||||||
on = function() end,
|
on = function() end,
|
||||||
|
add_to_config_reload_watch_list = function() end,
|
||||||
action = setmetatable({}, {
|
action = setmetatable({}, {
|
||||||
__index = function(_, k)
|
__index = function(_, k)
|
||||||
return function(...) return { action = k, args = {...} } end
|
return function(...) return { action = k, args = {...} } end
|
||||||
@@ -397,22 +398,15 @@ function cleanupOldThemes(themesDir: string, keepFile: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
function syncTheme(ctx: any) {
|
||||||
pi.on("session_start", async (_event, ctx) => {
|
|
||||||
const configDir = findConfigDir();
|
const configDir = findConfigDir();
|
||||||
if (!configDir) {
|
if (!configDir) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lua = findLua();
|
const lua = findLua();
|
||||||
if (!lua) {
|
if (!lua) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = getWeztermColors(configDir, lua);
|
const colors = getWeztermColors(configDir, lua);
|
||||||
if (!colors) {
|
if (!colors) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themesDir = join(homedir(), ".pi", "agent", "themes");
|
const themesDir = join(homedir(), ".pi", "agent", "themes");
|
||||||
if (!existsSync(themesDir)) {
|
if (!existsSync(themesDir)) {
|
||||||
@@ -440,5 +434,28 @@ export default function (pi: ExtensionAPI) {
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
ctx.ui.notify(`WezTerm theme sync failed: ${result.error}`, "error");
|
ctx.ui.notify(`WezTerm theme sync failed: ${result.error}`, "error");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
let currentCtx: any = null;
|
||||||
|
|
||||||
|
pi.on("session_start", async (_event, ctx) => {
|
||||||
|
currentCtx = ctx;
|
||||||
|
syncTheme(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch theme-state file for dark/light toggle changes
|
||||||
|
const stateFile = join(homedir(), ".config", "theme-state");
|
||||||
|
try {
|
||||||
|
watch(stateFile, { persistent: false }, (_event) => {
|
||||||
|
// Debounce: wait a tick for the file write to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentCtx) {
|
||||||
|
syncTheme(currentCtx);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// File may not exist yet — non-fatal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
{
|
{
|
||||||
"providers": {
|
"providers": {
|
||||||
|
"llama-serve": {
|
||||||
|
"baseUrl": "http://localhost:8080/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "dummy",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "Qwen/Qwen3-9B",
|
||||||
|
"name": "Qwen3.5-9B (Unsloth)",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": ["text"],
|
||||||
|
"contextWindow": 131072,
|
||||||
|
"maxTokens": 8192,
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"opencode-go": {
|
"opencode-go": {
|
||||||
"baseUrl": "https://opencode.ai/zen/go/v1",
|
"baseUrl": "https://opencode.ai/zen/go/v1",
|
||||||
"api": "openai-completions",
|
"api": "openai-completions",
|
||||||
|
|||||||
755
pi/.pi/agent/pi-crash.log
Normal file
755
pi/.pi/agent/pi-crash.log
Normal file
@@ -0,0 +1,755 @@
|
|||||||
|
Crash at 2026-04-24T12:06:17.955Z
|
||||||
|
Terminal width: 114
|
||||||
|
Line 451 visible width: 121
|
||||||
|
|
||||||
|
=== All rendered lines ===
|
||||||
|
[0] (w=0) [0m]8;;
|
||||||
|
[1] (w=114) [1m[38;2;123;79;196mpi[39m[22m[38;2;156;153;149m v0.67.68[39m [0m]8;;
|
||||||
|
[2] (w=114) [38;2;156;153;149mescape[39m[38;2;115;113;110m interrupt[39m[38;2;115;113;110m · [39m[38;2;156;153;149mctrl+c/ctrl+d[39m[38;2;115;113;110m clear/exit[39m[38;2;115;113;110m · [39m[38;2;156;153;149m/[39m[38;2;115;113;110m commands[39m[38;2;115;113;110m · [39m[38;2;156;153;149m![39m[38;2;115;113;110m bash[39m[38;2;115;113;110m · [39m[38;2;156;153;149mctrl+o[39m[38;2;115;113;110m more[39m [0m]8;;
|
||||||
|
[3] (w=114) [38;2;156;153;149mPress ctrl+o to show full startup help and loaded resources.[39m [0m]8;;
|
||||||
|
[4] (w=114) [0m]8;;
|
||||||
|
[5] (w=114) [38;2;156;153;149mPi can explain its own features and look up its docs. Ask it how to use or extend Pi.[39m [0m]8;;
|
||||||
|
[6] (w=0) [0m]8;;
|
||||||
|
[7] (w=114) [38;2;184;137;15m[Skills][39m [0m]8;;
|
||||||
|
[8] (w=114) [38;2;156;153;149m add-agent, ask-claude, homeassistant-ev, implementor, local-scout, opty, qmd, subagent-implement, subagent-plan,[0m]8;;
|
||||||
|
[9] (w=114) [38;2;156;153;149msubagent-review[39m [0m]8;;
|
||||||
|
[10] (w=0) [0m]8;;
|
||||||
|
[11] (w=114) [38;2;184;137;15m[Prompts][39m [0m]8;;
|
||||||
|
[12] (w=114) [38;2;156;153;149m /implement, /implement-critical, /plan, /review[39m [0m]8;;
|
||||||
|
[13] (w=0) [0m]8;;
|
||||||
|
[14] (w=114) [38;2;184;137;15m[Extensions][39m [0m]8;;
|
||||||
|
[15] (w=114) [38;2;156;153;149m @aliou/pi-guardrails:src, @benvargas/pi-exa-mcp, ask-claude.ts, chat-claude.ts, footer-display.ts, [0m]8;;
|
||||||
|
[16] (w=114) [38;2;156;153;149mgit-checkout-guard.ts, new-with-context.ts, pi, pi-ask-tool/index.ts, pi-claude-bridge, pi-subagents, [0m]8;;
|
||||||
|
[17] (w=114) [38;2;156;153;149mpi-subagents:notify.ts, postpone.ts, usage-bars/index.ts, wezterm-theme-sync/index.ts, worktree.ts[39m [0m]8;;
|
||||||
|
[18] (w=0) [0m]8;;
|
||||||
|
[19] (w=114) [38;2;184;137;15m[Themes][39m [0m]8;;
|
||||||
|
[20] (w=114) [38;2;156;153;149m wezterm-sync-9a35138e[39m [0m]8;;
|
||||||
|
[21] (w=0) [0m]8;;
|
||||||
|
[22] (w=0) [0m]8;;
|
||||||
|
[23] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[24] (w=114) [1m[38;2;184;137;15mUpdate Available[39m[22m [0m]8;;
|
||||||
|
[25] (w=114) [38;2;115;113;110mNew version 0.70.0 is available. [39m[38;2;123;79;196mRun: npm install -g @mariozechner/pi-coding-agent[39m [0m]8;;
|
||||||
|
[26] (w=114) [38;2;115;113;110mChangelog: [39m[38;2;123;79;196mhttps://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md[39m [0m]8;;
|
||||||
|
[27] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[28] (w=0) [0m]8;;
|
||||||
|
[29] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[30] (w=114) [1m[38;2;184;137;15mPackage Updates Available[39m[22m [0m]8;;
|
||||||
|
[31] (w=114) [38;2;115;113;110mPackage updates are available. Run [39m[38;2;123;79;196mpi update[39m [0m]8;;
|
||||||
|
[32] (w=114) [38;2;115;113;110mPackages:[39m [0m]8;;
|
||||||
|
[33] (w=114) - pi-subagents [0m]8;;
|
||||||
|
[34] (w=114) - @aliou/pi-guardrails [0m]8;;
|
||||||
|
[35] (w=114) - pi-claude-bridge [0m]8;;
|
||||||
|
[36] (w=114) [38;2;184;137;15m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[39m[0m]8;;
|
||||||
|
[37] (w=0) [0m]8;;
|
||||||
|
[38] (w=114) [38;2;156;153;149mEntered chat mode: Claude Opus · resume c9492aa2[39m [0m]8;;
|
||||||
|
[39] (w=0) [0m]8;;
|
||||||
|
[40] (w=114) [38;5;208m╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮[0m[0m]8;;
|
||||||
|
[41] (w=114) [38;5;208m│[0m [38;5;208m[1m▶ you[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[42] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[43] (w=114) [38;5;208m│[0m @agent/extensions/chat-claude.ts truncate all read outputs to max 40 lines. Last line, if truncated, should [38;5;208m│[0m[0m]8;;
|
||||||
|
[44] (w=114) [38;5;208m│[0m be something like '+N lines more lines' or better wording. Last line should be centered [38;5;208m│[0m[0m]8;;
|
||||||
|
[45] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[46] (w=114) [38;5;208m│[0m [38;5;208m◆ [0m[38;5;208m[1mClaude Opus[0m[38;2;156;153;149m session:c9492aa2[39m [38;5;208m│[0m[0m]8;;
|
||||||
|
[47] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[48] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/dotfiles/pi/.pi/agent -type d[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[49] (w=114) [38;5;208m│[0m [48;2;224;233;218m <persisted-output> [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[50] (w=114) [38;5;208m│[0m [48;2;224;233;218m Output too large (189KB). Full output saved to: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[51] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/.claude/projects/-home-jonas-dotfiles-pi--pi/c9492aa2-34ca-4d36-a541-83964a2ce1d8/tool-results [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[52] (w=114) [38;5;208m│[0m [48;2;224;233;218m /bh1bg7uzg.txt [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[53] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[54] (w=114) [38;5;208m│[0m [48;2;224;233;218m Preview (first 2KB): [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[55] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[56] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[57] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[58] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl-- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[59] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/subagent-artifacts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[60] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[61] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[62] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[63] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/b7684b6a [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[64] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[65] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/b7684b6a/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[66] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[67] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/95023efc [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[68] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[69] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/95023efc/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[70] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[71] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/5be564de [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[72] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T09-46-24-169Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[73] (w=114) [38;5;208m│[0m [48;2;224;233;218m _383b9ff3-7faa-4782-a731-e45cec0c8197/5be564de/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[74] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[75] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[76] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[77] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76/294d75f5 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[78] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/sessions/--home-jonas-projects-snow_trail_sdl--/2026-03-28T11-01-06-232Z [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[79] (w=114) [38;5;208m│[0m [48;2;224;233;218m _cae41525-1f66-4dbd-a0da-7c12c770aa76/294d75f5/run-0 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[80] (w=114) [38;5;208m│[0m [48;2;224;233;218m ... [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[81] (w=114) [38;5;208m│[0m [48;2;224;233;218m </persisted-output> [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[82] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[83] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ ls /home/jonas/dotfiles/pi/.pi/agent/extensions/ /home/jonas/dotfiles/pi/.pi/agent/shared/ 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[84] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/extensions/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[85] (w=114) [38;5;208m│[0m [48;2;224;233;218m ask-claude.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[86] (w=114) [38;5;208m│[0m [48;2;224;233;218m chat-claude.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[87] (w=114) [38;5;208m│[0m [48;2;224;233;218m exa-mcp.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[88] (w=114) [38;5;208m│[0m [48;2;224;233;218m footer-display.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[89] (w=114) [38;5;208m│[0m [48;2;224;233;218m git-checkout-guard.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[90] (w=114) [38;5;208m│[0m [48;2;224;233;218m new-with-context.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[91] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-mcp [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[92] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-tool [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[93] (w=114) [38;5;208m│[0m [48;2;224;233;218m postpone.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[94] (w=114) [38;5;208m│[0m [48;2;224;233;218m usage-bars [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[95] (w=114) [38;5;208m│[0m [48;2;224;233;218m wezterm-theme-sync [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[96] (w=114) [38;5;208m│[0m [48;2;224;233;218m worktree.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[97] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[98] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/shared/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[99] (w=114) [38;5;208m│[0m [48;2;224;233;218m claude-stream.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[100] (w=114) [38;5;208m│[0m [48;2;224;233;218m pi-ask-bridge.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[101] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[102] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/shared/claude-stream.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[103] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[104] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m * claude-stream — Shared types, rendering, and core spawn/stream logic [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[105] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * for ask-claude and chat-claude pi extensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[106] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m * [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[107] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m * Both extensions spawn `claude -p --output-format stream-json` and parse [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[108] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m * the same streaming protocol. This module provides: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[109] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m * - Block types (ThinkingBlock, ToolBlock, TextBlock) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[110] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m * - Rendering helpers (tool call lines, result boxes, usage formatting) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[111] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m * - runClaude() — the core spawn + stream parser [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[112] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 10[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[113] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 11[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[114] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 12[39m import { spawn } from "node:child_process"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[115] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 13[39m import { readFileSync } from "node:fs"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[116] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 14[39m import { diffLines } from "diff"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[117] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 15[39m import { getMarkdownTheme } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[118] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 16[39m import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[119] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 17[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[120] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 18[39m // ============================================================================= [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[121] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 19[39m // Block types [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[122] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 20[39m // ============================================================================= [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[123] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 21[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[124] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 22[39m export interface ThinkingBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[125] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 23[39m type: "thinking"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[126] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 24[39m text: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[127] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 25[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[128] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 26[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[129] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 27[39m export interface ToolBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[130] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 28[39m type: "tool"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[131] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 29[39m id: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[132] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 30[39m name: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[133] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 31[39m inputJson: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[134] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 32[39m editContext?: { before: string[]; after: string[]; startLine: number }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[135] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 33[39m result?: { text: string; isError: boolean }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[136] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 34[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[137] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 35[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[138] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 36[39m export interface TextBlock { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[139] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 37[39m type: "text"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[140] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m text: string; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[141] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[142] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[143] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 726 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[144] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[145] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/dotfiles/pi/.pi/agent -name "node_modules" -prune -o -name "package.json" -print [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[146] (w=114) [38;5;208m│[0m [48;5;130m [1;97m2>/dev/null | head -5[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[147] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/extensions/pi-ask-mcp/package.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[148] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[149] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas -path "*/node_modules/@mariozechner/pi-tui*" -name "*.d.ts" 2>/dev/null | head -10[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[150] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/box.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[151] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[152] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[153] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[154] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[155] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[156] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[157] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[158] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[159] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/spacer.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[160] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[161] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ find /home/jonas/node_modules/@mariozechner/pi-tui/dist/components -name "text*"[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[162] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.js [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[163] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[164] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.js.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[165] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[166] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[167] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/text.d.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[168] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import type { Component } from "../tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[169] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[170] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Text component - displays multi-line text with word wrapping [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[171] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[172] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export declare class Text implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[173] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m private text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[174] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m private paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[175] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m private paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[176] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m private customBgFn?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[177] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m private cachedText?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[178] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m private cachedWidth?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[179] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m private cachedLines?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[180] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[181] (w=114) [38;5;208m│[0m [48;2;224;233;218m string); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[182] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m setText(text: string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[183] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m setCustomBgFn(customBgFn?: (text: string) => string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[184] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[185] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[186] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[187] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m //# sourceMappingURL=text.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[188] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[189] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/text.js[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[190] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[191] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[192] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Text component - displays multi-line text with word wrapping [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[193] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[194] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export class Text { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[195] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[196] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m paddingX; // Left/right padding [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[197] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m paddingY; // Top/bottom padding [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[198] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[199] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m // Cache for rendered output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[200] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m cachedText; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[201] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m cachedWidth; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[202] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m cachedLines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[203] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m constructor(text = "", paddingX = 1, paddingY = 1, customBgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[204] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m this.text = text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[205] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m this.paddingX = paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[206] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m this.paddingY = paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[207] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m this.customBgFn = customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[208] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[209] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m20[39m setText(text) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[210] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m21[39m this.text = text; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[211] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m22[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[212] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m23[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[213] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m24[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[214] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m25[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[215] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m26[39m setCustomBgFn(customBgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[216] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m27[39m this.customBgFn = customBgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[217] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m28[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[218] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m29[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[219] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m30[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[220] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m31[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[221] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m32[39m invalidate() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[222] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m33[39m this.cachedText = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[223] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m34[39m this.cachedWidth = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[224] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m35[39m this.cachedLines = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[225] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m36[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[226] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m37[39m render(width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[227] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m38[39m // Check cache [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[228] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m39[39m if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[229] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m40[39m return this.cachedLines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[230] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 49 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[231] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[232] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/box.js[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[233] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import { applyBackgroundToLine, visibleWidth } from "../utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[234] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[235] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Box component - a container that applies padding and background to all children [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[236] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[237] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export class Box { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[238] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[239] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[240] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[241] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[242] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 10[39m // Cache for rendered output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[243] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 11[39m cache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[244] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 12[39m constructor(paddingX = 1, paddingY = 1, bgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[245] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 13[39m this.paddingX = paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[246] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 14[39m this.paddingY = paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[247] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 15[39m this.bgFn = bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[248] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 16[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[249] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 17[39m addChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[250] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 18[39m this.children.push(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[251] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 19[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[252] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 20[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[253] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 21[39m removeChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[254] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 22[39m const index = this.children.indexOf(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[255] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 23[39m if (index !== -1) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[256] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 24[39m this.children.splice(index, 1); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[257] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 25[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[258] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 26[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[259] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 27[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[260] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 28[39m clear() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[261] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 29[39m this.children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[262] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 30[39m this.invalidateCache(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[263] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 31[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[264] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 32[39m setBgFn(bgFn) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[265] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 33[39m this.bgFn = bgFn; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[266] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 34[39m // Don't invalidate here - we'll detect bgFn changes by sampling output [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[267] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 35[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[268] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 36[39m invalidateCache() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[269] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 37[39m this.cache = undefined; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[270] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[271] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m matchCache(width, childLines, bgSample) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[272] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m const cache = this.cache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[273] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 64 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[274] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[275] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cat /home/jonas/node_modules/@mariozechner/pi-tui/dist/index.d.ts 2>/dev/null | head -50[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[276] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type AutocompleteItem, type AutocompleteProvider, CombinedAutocompleteProvider, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[277] (w=114) [38;5;208m│[0m [48;2;224;233;218m SlashCommand, } from "./autocomplete.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[278] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Box } from "./components/box.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[279] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { CancellableLoader } from "./components/cancellable-loader.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[280] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Editor, type EditorOptions, type EditorTheme } from "./components/editor.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[281] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Image, type ImageOptions, type ImageTheme } from "./components/image.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[282] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Input } from "./components/input.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[283] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Loader } from "./components/loader.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[284] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[285] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type SelectItem, SelectList, type SelectListTheme } from "./components/select-list.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[286] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type SettingItem, SettingsList, type SettingsListTheme } from "./components/settings-list.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[287] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Spacer } from "./components/spacer.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[288] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { Text } from "./components/text.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[289] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { TruncatedText } from "./components/truncated-text.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[290] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type { EditorComponent } from "./editor-component.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[291] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from "./fuzzy.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[292] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { DEFAULT_EDITOR_KEYBINDINGS, type EditorAction, type EditorKeybindingsConfig, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[293] (w=114) [38;5;208m│[0m [48;2;224;233;218m EditorKeybindingsManager, getEditorKeybindings, setEditorKeybindings, } from "./keybindings.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[294] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { isKeyRelease, isKeyRepeat, isKittyProtocolActive, Key, type KeyEventType, type KeyId, matchesKey, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[295] (w=114) [38;5;208m│[0m [48;2;224;233;218m parseKey, setKittyProtocolActive, } from "./keys.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[296] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[297] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { ProcessTerminal, type Terminal } from "./terminal.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[298] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { allocateImageId, type CellDimensions, calculateImageRows, deleteAllKittyImages, deleteKittyImage, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[299] (w=114) [38;5;208m│[0m [48;2;224;233;218m detectCapabilities, encodeITerm2, encodeKitty, getCapabilities, getCellDimensions, getGifDimensions, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[300] (w=114) [38;5;208m│[0m [48;2;224;233;218m getImageDimensions, getJpegDimensions, getPngDimensions, getWebpDimensions, type ImageDimensions, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[301] (w=114) [38;5;208m│[0m [48;2;224;233;218m ImageProtocol, type ImageRenderOptions, imageFallback, renderImage, resetCapabilitiesCache, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[302] (w=114) [38;5;208m│[0m [48;2;224;233;218m setCellDimensions, type TerminalCapabilities, } from "./terminal-image.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[303] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { type Component, Container, CURSOR_MARKER, type Focusable, isFocusable, type OverlayAnchor, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[304] (w=114) [38;5;208m│[0m [48;2;224;233;218m OverlayHandle, type OverlayMargin, type OverlayOptions, type SizeValue, TUI, } from "./tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[305] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[306] (w=114) [38;5;208m│[0m [48;2;224;233;218m //# sourceMappingURL=index.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[307] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[308] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cat /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.d.ts | head -100[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[309] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[310] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Minimal TUI implementation with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[311] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[312] (w=114) [38;5;208m│[0m [48;2;224;233;218m import type { Terminal } from "./terminal.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[313] (w=114) [38;5;208m│[0m [48;2;224;233;218m import { visibleWidth } from "./utils.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[314] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[315] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Component interface - all components must implement this [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[316] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[317] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[318] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[319] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Render the component to lines for the given viewport width [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[320] (w=114) [38;5;208m│[0m [48;2;224;233;218m * @param width - Current viewport width [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[321] (w=114) [38;5;208m│[0m [48;2;224;233;218m * @returns Array of strings, each representing a line [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[322] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[323] (w=114) [38;5;208m│[0m [48;2;224;233;218m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[324] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[325] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Optional handler for keyboard input when component has focus [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[326] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[327] (w=114) [38;5;208m│[0m [48;2;224;233;218m handleInput?(data: string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[328] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[329] (w=114) [38;5;208m│[0m [48;2;224;233;218m * If true, component receives key release events (Kitty protocol). [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[330] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Default is false - release events are filtered out. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[331] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[332] (w=114) [38;5;208m│[0m [48;2;224;233;218m wantsKeyRelease?: boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[333] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[334] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Invalidate any cached rendering state. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[335] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Called when theme changes or when component needs to re-render from scratch. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[336] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[337] (w=114) [38;5;208m│[0m [48;2;224;233;218m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[338] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[339] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[340] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Interface for components that can receive focus and display a hardware cursor. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[341] (w=114) [38;5;208m│[0m [48;2;224;233;218m * When focused, the component should emit CURSOR_MARKER at the cursor position [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[342] (w=114) [38;5;208m│[0m [48;2;224;233;218m * in its render output. TUI will find this marker and position the hardware [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[343] (w=114) [38;5;208m│[0m [48;2;224;233;218m * cursor there for proper IME candidate window positioning. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[344] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[345] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface Focusable { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[346] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[347] (w=114) [38;5;208m│[0m [48;2;224;233;218m focused: boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[348] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[349] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Type guard to check if a component implements Focusable */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[350] (w=114) [38;5;208m│[0m [48;2;224;233;218m export declare function isFocusable(component: Component | null): component is Component & Focusable; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[351] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[352] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Cursor position marker - APC (Application Program Command) sequence. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[353] (w=114) [38;5;208m│[0m [48;2;224;233;218m * This is a zero-width escape sequence that terminals ignore. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[354] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Components emit this at the cursor position when focused. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[355] (w=114) [38;5;208m│[0m [48;2;224;233;218m * TUI finds and strips this marker, then positions the hardware cursor there. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[356] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[357] (w=114) [38;5;208m│[0m [48;2;224;233;218m export declare const CURSOR_MARKER = "\u001B_pi:c\u0007"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[358] (w=114) [38;5;208m│[0m [48;2;224;233;218m export { visibleWidth }; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[359] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[360] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Anchor position for overlays [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[361] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[362] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[363] (w=114) [38;5;208m│[0m [48;2;224;233;218m "top-center" | "bottom-center" | "left-center" | "right-center"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[364] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[365] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Margin configuration for overlays [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[366] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[367] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayMargin { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[368] (w=114) [38;5;208m│[0m [48;2;224;233;218m top?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[369] (w=114) [38;5;208m│[0m [48;2;224;233;218m right?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[370] (w=114) [38;5;208m│[0m [48;2;224;233;218m bottom?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[371] (w=114) [38;5;208m│[0m [48;2;224;233;218m left?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[372] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[373] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Value that can be absolute (number) or percentage (string like "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[374] (w=114) [38;5;208m│[0m [48;2;224;233;218m export type SizeValue = number | `${number}%`; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[375] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[376] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Options for overlay positioning and sizing. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[377] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Values can be absolute numbers or percentage strings (e.g., "50%"). [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[378] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[379] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayOptions { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[380] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Width in columns, or percentage of terminal width (e.g., "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[381] (w=114) [38;5;208m│[0m [48;2;224;233;218m width?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[382] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Minimum width in columns */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[383] (w=114) [38;5;208m│[0m [48;2;224;233;218m minWidth?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[384] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[385] (w=114) [38;5;208m│[0m [48;2;224;233;218m maxHeight?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[386] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Anchor point for positioning (default: 'center') */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[387] (w=114) [38;5;208m│[0m [48;2;224;233;218m anchor?: OverlayAnchor; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[388] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Horizontal offset from anchor position (positive = right) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[389] (w=114) [38;5;208m│[0m [48;2;224;233;218m offsetX?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[390] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Vertical offset from anchor position (positive = down) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[391] (w=114) [38;5;208m│[0m [48;2;224;233;218m offsetY?: number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[392] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[393] (w=114) [38;5;208m│[0m [48;2;224;233;218m row?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[394] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[395] (w=114) [38;5;208m│[0m [48;2;224;233;218m col?: SizeValue; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[396] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Margin from terminal edges. Number applies to all sides. */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[397] (w=114) [38;5;208m│[0m [48;2;224;233;218m margin?: OverlayMargin | number; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[398] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[399] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Control overlay visibility based on terminal dimensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[400] (w=114) [38;5;208m│[0m [48;2;224;233;218m * If provided, overlay is only rendered when this returns true. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[401] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Called each render cycle with current terminal dimensions. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[402] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[403] (w=114) [38;5;208m│[0m [48;2;224;233;218m visible?: (termWidth: number, termHeight: number) => boolean; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[404] (w=114) [38;5;208m│[0m [48;2;224;233;218m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[405] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[406] (w=114) [38;5;208m│[0m [48;2;224;233;218m * Handle returned by showOverlay for controlling the overlay [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[407] (w=114) [38;5;208m│[0m [48;2;224;233;218m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[408] (w=114) [38;5;208m│[0m [48;2;224;233;218m export interface OverlayHandle { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[409] (w=114) [38;5;208m│[0m [48;2;224;233;218m /** Permanently remove the overlay (cannot be shown again) */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[410] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[411] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ grep -n "class Container" /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.js | head -5[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[412] (w=114) [38;5;208m│[0m [48;2;224;233;218m 38:export class Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[413] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[414] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/tui.js:38-117[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[415] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 38[39m export class Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[416] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 39[39m children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[417] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 40[39m addChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[418] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 41[39m this.children.push(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[419] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 42[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[420] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 43[39m removeChild(component) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[421] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 44[39m const index = this.children.indexOf(component); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[422] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 45[39m if (index !== -1) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[423] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 46[39m this.children.splice(index, 1); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[424] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 47[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[425] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 48[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[426] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 49[39m clear() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[427] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 50[39m this.children = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[428] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 51[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[429] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 52[39m invalidate() { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[430] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 53[39m for (const child of this.children) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[431] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 54[39m child.invalidate?.(); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[432] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 55[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[433] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 56[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[434] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 57[39m render(width) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[435] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 58[39m const lines = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[436] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 59[39m for (const child of this.children) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[437] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 60[39m lines.push(...child.render(width)); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[438] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 61[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[439] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 62[39m return lines; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[440] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 63[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[441] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 64[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[442] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 65[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[443] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 66[39m * TUI - Main class for managing terminal UI with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[444] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 67[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[445] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 68[39m export class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[446] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 69[39m terminal; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[447] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 70[39m previousLines = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[448] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 71[39m previousWidth = 0; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[449] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 72[39m previousHeight = 0; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[450] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 73[39m focusedComponent = null; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[451] (w=121) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 74[39m /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */[49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[452] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 75[39m onDebug; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[453] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 76[39m renderRequested = false; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[454] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 77[39m cursorRow = 0; // Logical cursor row (end of rendered content) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[455] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 40 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[456] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[457] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ grep -n "Container" /home/jonas/node_modules/@mariozechner/pi-tui/dist/tui.d.ts | head -20[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[458] (w=114) [38;5;208m│[0m [48;2;224;233;218m 108: * Container - a component that contains other components [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[459] (w=114) [38;5;208m│[0m [48;2;224;233;218m 110:export declare class Container implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[460] (w=114) [38;5;208m│[0m [48;2;224;233;218m 121:export declare class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[461] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[462] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/tui.d.ts:108-122[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[463] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m108[39m * Container - a component that contains other components [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[464] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m109[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[465] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m110[39m export declare class Container implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[466] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m111[39m children: Component[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[467] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m112[39m addChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[468] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m113[39m removeChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[469] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m114[39m clear(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[470] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m115[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[471] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m116[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[472] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m117[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[473] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m118[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[474] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m119[39m * TUI - Main class for managing terminal UI with differential rendering [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[475] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m120[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[476] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m121[39m export declare class TUI extends Container { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[477] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m122[39m terminal: Terminal; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[478] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[479] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/node_modules/@mariozechner/pi-tui/dist/components/box.d.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[480] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 1[39m import type { Component } from "../tui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[481] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 2[39m /** [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[482] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 3[39m * Box component - a container that applies padding and background to all children [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[483] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 4[39m */ [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[484] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 5[39m export declare class Box implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[485] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 6[39m children: Component[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[486] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 7[39m private paddingX; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[487] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 8[39m private paddingY; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[488] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 9[39m private bgFn?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[489] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m10[39m private cache?; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[490] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m11[39m constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[491] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m12[39m addChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[492] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m13[39m removeChild(component: Component): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[493] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m14[39m clear(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[494] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m15[39m setBgFn(bgFn?: (text: string) => string): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[495] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m16[39m private invalidateCache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[496] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m17[39m private matchCache; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[497] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m18[39m invalidate(): void; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[498] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m19[39m render(width: number): string[]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[499] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m20[39m private applyBg; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[500] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m21[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[501] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m22[39m //# sourceMappingURL=box.d.ts.map [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[502] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[503] (w=114) [38;5;208m│[0m Now I have enough understanding. Let me look at the exact current state of the tool-block rendering site and [38;5;208m│[0m[0m]8;;
|
||||||
|
[504] (w=114) [38;5;208m│[0m the imports: [38;5;208m│[0m[0m]8;;
|
||||||
|
[505] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[506] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:NaN-NaN[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[507] (w=114) [38;5;208m│[0m [48;2;245;221;218m [38;2;156;153;149m[39m [1;91mInputValidationError: Read failed due to the following issue: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[508] (w=114) [38;5;208m│[0m [48;2;245;221;218m [1;91m[38;2;156;153;149m[39m The parameter `offset` type is expected as `number` but provided as `string`[22;23;24;39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[509] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[510] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:30-41[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[511] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m30[39m import { CustomEditor, getMarkdownTheme } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[512] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m31[39m import type { ExtensionAPI, KeybindingsManager } from "@mariozechner/pi-coding-agent"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[513] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m32[39m import { Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type EditorTheme } from [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[514] (w=114) [38;5;208m│[0m [48;2;224;233;218m "@mariozechner/pi-tui"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[515] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m33[39m import { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[516] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m34[39m formatUsage, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[517] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m35[39m renderToolBlock, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[518] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m36[39m runClaude, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[519] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m37[39m type StreamBlock, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[520] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m38[39m } from "../shared/claude-stream.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[521] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m39[39m import { startAskBridge, type AskBridge } from "../shared/pi-ask-bridge.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[522] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m40[39m import { askSingleQuestionWithInlineNote } from "./pi-ask-tool/ask-inline-ui.js"; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[523] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m41[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[524] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[525] (w=114) [38;5;208m│[0m Now I'll make the changes. First, add [38;2;123;79;196mBox[39m and [38;2;123;79;196mComponent[39m to the imports: [38;5;208m│[0m[0m]8;;
|
||||||
|
[526] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[527] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[528] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,1 +1,1 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[529] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-import { Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type EditorTheme } from [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[530] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m"@mariozechner/pi-tui";[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[531] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+import { Box, Container, matchesKey, Markdown, Spacer, Text, TUI, visibleWidth, type Component, type [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[532] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82mEditorTheme } from "@mariozechner/pi-tui";[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[533] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[534] (w=114) [38;5;208m│[0m Now add the constant and the truncation helper. I'll place it right after the orange-border helper, where [38;5;208m│[0m[0m]8;;
|
||||||
|
[535] (w=114) [38;5;208m│[0m rendering helpers live: [38;5;208m│[0m[0m]8;;
|
||||||
|
[536] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[537] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[538] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,3 +1,82 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[539] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[540] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// Models / turn types[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[541] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m-// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[542] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[543] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Read-tool result truncation[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[544] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+//[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[545] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// `Read` tool calls inside chat mode often dump entire files into the result[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[546] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// banner — many hundreds of lines, which buries the surrounding conversation.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[547] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// We cap the rendered file content at MAX_READ_LINES and append a single[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[548] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// centered notice line describing how many lines were hidden. This is a[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[549] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// PRESENTATION-only truncation: `block.result.text` is left untouched, so[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[550] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// resumed sessions / re-renders still see the full content.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[551] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+//[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[552] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Centering needs render-time width, so we implement a tiny custom Component[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[553] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// (TruncatedReadResult) and swap it into the Box body produced by the shared[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[554] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// renderToolBlock helper. The same dim line-number formatting used by[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[555] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// renderToolResultBox is preserved so the truncated view looks identical to[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[556] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// the un-truncated one above the notice.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[557] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[558] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+const MAX_READ_LINES = 40;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[559] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[560] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+class TruncatedReadResult implements Component {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[561] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ constructor([39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[562] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly numbered: { num: string; content: string }[],[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[563] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly maxNumLen: number,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[564] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly dimFn: (s: string) => string,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[565] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ private readonly noticeFn: (s: string) => string,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[566] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ ) {}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[567] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[568] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ invalidate(): void { /* stateless */ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[569] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[570] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ render(width: number): string[] {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[571] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const total = this.numbered.length;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[572] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const visible = Math.min(MAX_READ_LINES, total);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[573] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const lines: string[] = [];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[574] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ for (let i = 0; i < visible; i++) {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[575] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const l = this.numbered[i];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[576] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ lines.push(this.dimFn(l.num.padStart(this.maxNumLen)) + " " + l.content);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[577] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[578] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (total > visible) {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[579] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const hidden = total - visible;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[580] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const notice = `… ${hidden} more line${hidden === 1 ? "" : "s"} hidden …`;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[581] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const visLen = visibleWidth(notice);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[582] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const left = Math.max(0, Math.floor((width - visLen) / 2));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[583] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ lines.push(" ".repeat(left) + this.noticeFn(notice));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[584] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[585] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return lines;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[586] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ }[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[587] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[588] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[589] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Wrap shared renderToolBlock: for `Read` tool blocks whose result exceeds[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[590] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// MAX_READ_LINES, replace the Box body's child Text with our truncating[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[591] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// component. All other tool kinds, error results, and short reads pass[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[592] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// through unchanged.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[593] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+function renderToolBlockTruncated(block: Extract<StreamBlock, { type: "tool" }>, theme: any): Container {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[594] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const c = renderToolBlock(block, theme);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[595] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (block.name.toLowerCase() !== "read") return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[596] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (!block.result || block.result.isError) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[597] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[598] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const rawLines = block.result.text.split("\n").filter((l) => l.length > 0);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[599] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (rawLines.length <= MAX_READ_LINES) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[600] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[601] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const parsed = rawLines.map((l) => {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[602] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const tab = l.indexOf("\t");[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[603] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return tab >= 0 ? { num: l.slice(0, tab), content: l.slice(tab + 1) } : { num: "", content: l };[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[604] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ });[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[605] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const maxNumLen = parsed.reduce((m, l) => Math.max(m, l.num.length), 0);[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[606] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[607] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ // renderToolBlock's container is [headerText, bodyBox]. Bail safely if a[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[608] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ // future change to that helper alters the structure.[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[609] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ const body = c.children[1];[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[610] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (!(body instanceof Box)) return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[611] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ body.clear();[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[612] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ body.addChild(new TruncatedReadResult([39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[613] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ parsed,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[614] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ maxNumLen,[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[615] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ (s) => theme.fg("dim", s),[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[616] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ (s) => theme.fg("dim", s),[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[617] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ ));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[618] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ return c;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[619] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+}[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[620] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[621] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[622] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// Models / turn types[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[623] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+// ---------------------------------------------------------------------------[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[624] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[625] (w=114) [38;5;208m│[0m Now swap the call site inside [38;2;123;79;196mrenderTurnInto[39m to use the wrapper: [38;5;208m│[0m[0m]8;;
|
||||||
|
[626] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[627] (w=114) [38;5;208m│[0m [48;5;130m [1;97medit ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[628] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m@@ -1,4 +1,4 @@[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[629] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- } else if (block.type === "tool") {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[630] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- if (addedAny) container.addChild(new Spacer(1));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[631] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- container.addChild(renderToolBlock(block, theme));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[632] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;209;52;79m- addedAny = true;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[633] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ } else if (block.type === "tool") {[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[634] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ if (addedAny) container.addChild(new Spacer(1));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[635] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ container.addChild(renderToolBlockTruncated(block, theme));[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[636] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;30;155;82m+ addedAny = true;[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[637] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[638] (w=114) [38;5;208m│[0m Now let me verify there's no TypeScript build/check available and the file still parses cleanly: [38;5;208m│[0m[0m]8;;
|
||||||
|
[639] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[640] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ ls /home/jonas/dotfiles/pi/.pi/ /home/jonas/dotfiles/pi/.pi/agent/ 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[641] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[642] (w=114) [38;5;208m│[0m [48;2;224;233;218m agent [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[643] (w=114) [38;5;208m│[0m [48;2;224;233;218m web-search.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[644] (w=114) [38;5;208m│[0m [48;2;224;233;218m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[645] (w=114) [38;5;208m│[0m [48;2;224;233;218m /home/jonas/dotfiles/pi/.pi/agent/: [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[646] (w=114) [38;5;208m│[0m [48;2;224;233;218m agents [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[647] (w=114) [38;5;208m│[0m [48;2;224;233;218m auth.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[648] (w=114) [38;5;208m│[0m [48;2;224;233;218m auth.json.current [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[649] (w=114) [38;5;208m│[0m [48;2;224;233;218m cache [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[650] (w=114) [38;5;208m│[0m [48;2;224;233;218m extensions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[651] (w=114) [38;5;208m│[0m [48;2;224;233;218m mcp-cache.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[652] (w=114) [38;5;208m│[0m [48;2;224;233;218m mcp.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[653] (w=114) [38;5;208m│[0m [48;2;224;233;218m models.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[654] (w=114) [38;5;208m│[0m [48;2;224;233;218m profiles [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[655] (w=114) [38;5;208m│[0m [48;2;224;233;218m prompts [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[656] (w=114) [38;5;208m│[0m [48;2;224;233;218m run-history.jsonl [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[657] (w=114) [38;5;208m│[0m [48;2;224;233;218m sessions [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[658] (w=114) [38;5;208m│[0m [48;2;224;233;218m settings.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[659] (w=114) [38;5;208m│[0m [48;2;224;233;218m shared [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[660] (w=114) [38;5;208m│[0m [48;2;224;233;218m skills [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[661] (w=114) [38;5;208m│[0m [48;2;224;233;218m themes [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[662] (w=114) [38;5;208m│[0m [48;2;224;233;218m usage-cache.json [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[663] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[664] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ which tsc 2>/dev/null; ls /home/jonas/node_modules/.bin/tsc 2>/dev/null[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[665] (w=114) [38;5;208m│[0m [48;2;245;221;218m Exit code 2 [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[666] (w=114) [38;5;208m│[0m [48;2;245;221;218m /home/jonas/.npm-global/bin/tsc [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[667] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[668] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ cd /home/jonas/dotfiles/pi/.pi/agent && tsc --noEmit --target esnext --module nodenext [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[669] (w=114) [38;5;208m│[0m [48;5;130m [1;97m--moduleResolution nodenext --esModuleInterop --skipLibCheck --strict false extensions/chat-claude.ts 2>&1 [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[670] (w=114) [38;5;208m│[0m [48;5;130m [1;97m| head -40[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[671] (w=114) [38;5;208m│[0m [48;2;224;233;218m shared/pi-ask-bridge.ts(117,37): error TS1470: The 'import.meta' meta-property is not allowed in files [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[672] (w=114) [38;5;208m│[0m [48;2;224;233;218m which will build into CommonJS output. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[673] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[674] (w=114) [38;5;208m│[0m [48;5;130m [1;97m$ tsc --noEmit --target esnext --module esnext --moduleResolution bundler --esModuleInterop --skipLibCheck [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[675] (w=114) [38;5;208m│[0m [48;5;130m [1;97m--strict false extensions/chat-claude.ts 2>&1 | head -40[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[676] (w=114) [38;5;208m│[0m [48;2;224;233;218m (Bash completed with no output) [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[677] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[678] (w=114) [38;5;208m│[0m No type errors. Let me show the final relevant sections: [38;5;208m│[0m[0m]8;;
|
||||||
|
[679] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[680] (w=114) [38;5;208m│[0m [48;5;130m [1;97mread ~/dotfiles/pi/.pi/agent/extensions/chat-claude.ts:72-153[22;23;24;39m [0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[681] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 72[39m // --------------------------------------------------------------------------- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[682] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 73[39m // Read-tool result truncation [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[683] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 74[39m // [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[684] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 75[39m // `Read` tool calls inside chat mode often dump entire files into the result [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[685] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 76[39m // banner — many hundreds of lines, which buries the surrounding conversation. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[686] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 77[39m // We cap the rendered file content at MAX_READ_LINES and append a single [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[687] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 78[39m // centered notice line describing how many lines were hidden. This is a [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[688] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 79[39m // PRESENTATION-only truncation: `block.result.text` is left untouched, so [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[689] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 80[39m // resumed sessions / re-renders still see the full content. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[690] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 81[39m // [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[691] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 82[39m // Centering needs render-time width, so we implement a tiny custom Component [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[692] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 83[39m // (TruncatedReadResult) and swap it into the Box body produced by the shared [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[693] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 84[39m // renderToolBlock helper. The same dim line-number formatting used by [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[694] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 85[39m // renderToolResultBox is preserved so the truncated view looks identical to [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[695] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 86[39m // the un-truncated one above the notice. [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[696] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 87[39m // --------------------------------------------------------------------------- [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[697] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 88[39m const MAX_READ_LINES = 40; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[698] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 89[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[699] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 90[39m class TruncatedReadResult implements Component { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[700] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 91[39m constructor( [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[701] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 92[39m private readonly numbered: { num: string; content: string }[], [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[702] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 93[39m private readonly maxNumLen: number, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[703] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 94[39m private readonly dimFn: (s: string) => string, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[704] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 95[39m private readonly noticeFn: (s: string) => string, [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[705] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 96[39m ) {} [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[706] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 97[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[707] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 98[39m invalidate(): void { /* stateless */ } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[708] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m 99[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[709] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m100[39m render(width: number): string[] { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[710] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m101[39m const total = this.numbered.length; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[711] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m102[39m const visible = Math.min(MAX_READ_LINES, total); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[712] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m103[39m const lines: string[] = []; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[713] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m104[39m for (let i = 0; i < visible; i++) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[714] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m105[39m const l = this.numbered[i]; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[715] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m106[39m lines.push(this.dimFn(l.num.padStart(this.maxNumLen)) + " " + l.content); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[716] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m107[39m } [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[717] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m108[39m if (total > visible) { [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[718] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m109[39m const hidden = total - visible; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[719] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m110[39m const notice = `… ${hidden} more line${hidden === 1 ? "" : "s"} hidden …`; [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[720] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m111[39m const visLen = visibleWidth(notice); [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[721] (w=114) [38;5;208m│[0m [48;2;224;233;218m [38;2;156;153;149m… 42 more lines hidden …[39m [49m [38;5;208m│[0m[0m]8;;
|
||||||
|
[722] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[723] (w=114) [38;5;208m│[0m Done. Summary of changes to [38;2;123;79;196m/home/jonas/dotfiles/pi/.pi/agent/extensions/chat-claude.ts[39m: [38;5;208m│[0m[0m]8;;
|
||||||
|
[724] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[725] (w=114) [38;5;208m│[0m [38;2;123;79;196m1. [39m[1mImports[22m — added [38;2;123;79;196mBox[39m and [38;2;123;79;196mtype Component[39m to the [38;2;123;79;196m@mariozechner/pi-tui[39m import. [38;5;208m│[0m[0m]8;;
|
||||||
|
[726] (w=114) [38;5;208m│[0m [38;2;123;79;196m2. [39m[1mNew constant [38;2;123;79;196mMAX_READ_LINES = 40[39m[22m — hard cap on rendered file lines per [38;2;123;79;196mRead[39m tool result. [38;5;208m│[0m[0m]8;;
|
||||||
|
[727] (w=114) [38;5;208m│[0m [38;2;123;79;196m3. [39m[1mNew [38;2;123;79;196mTruncatedReadResult[39m component[22m — a tiny [38;2;123;79;196mComponent[39m that renders the first 40 numbered lines using the [38;5;208m│[0m[0m]8;;
|
||||||
|
[728] (w=114) [38;5;208m│[0m same dim line-number formatting as the shared [38;2;123;79;196mrenderToolResultBox[39m, then appends a single centered notice [38;5;208m│[0m[0m]8;;
|
||||||
|
[729] (w=114) [38;5;208m│[0m line ([38;2;123;79;196m… N more lines hidden …[39m). Centering is computed from the render-time [38;2;123;79;196mwidth[39m, so it tracks terminal [38;5;208m│[0m[0m]8;;
|
||||||
|
[730] (w=114) [38;5;208m│[0m resizes. [38;5;208m│[0m[0m]8;;
|
||||||
|
[731] (w=114) [38;5;208m│[0m [38;2;123;79;196m4. [39m[1mNew [38;2;123;79;196mrenderToolBlockTruncated()[39m wrapper[22m — calls the shared [38;2;123;79;196mrenderToolBlock[39m, and only for [38;2;123;79;196mread[39m tool blocks [38;5;208m│[0m[0m]8;;
|
||||||
|
[732] (w=114) [38;5;208m│[0m whose result exceeds 40 lines, swaps out the Box body's [38;2;123;79;196mText[39m child with [38;2;123;79;196mTruncatedReadResult[39m. Errors, short [38;5;208m│[0m[0m]8;;
|
||||||
|
[733] (w=114) [38;5;208m│[0m reads, and every non-read tool pass through unchanged. The original [38;2;123;79;196mblock.result.text[39m is never mutated, so [38;5;208m│[0m[0m]8;;
|
||||||
|
[734] (w=114) [38;5;208m│[0m resumed sessions and re-renders still see the full content. [38;5;208m│[0m[0m]8;;
|
||||||
|
[735] (w=114) [38;5;208m│[0m [38;2;123;79;196m5. [39m[1mSingle call-site swap[22m in [38;2;123;79;196mrenderTurnInto[39m: [38;2;123;79;196mrenderToolBlock(block, theme)[39m → [38;2;123;79;196mrenderToolBlockTruncated(block, [38;5;208m│[0m[0m]8;;
|
||||||
|
[736] (w=114) [38;5;208m│[0m [38;2;123;79;196mtheme)[39m. [38;5;208m│[0m[0m]8;;
|
||||||
|
[737] (w=114) [38;5;208m│[0m [38;5;208m│[0m[0m]8;;
|
||||||
|
[738] (w=114) [38;5;208m│[0m [38;2;123;79;196mtsc --noEmit[39m on the file produces no errors. [38;5;208m│[0m[0m]8;;
|
||||||
|
[739] (w=114) [38;5;208m╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯[0m[0m]8;;
|
||||||
|
[740] (w=0) [0m]8;;
|
||||||
|
[741] (w=114) [38;2;156;153;149mResuming session c9492aa2 (1h ago): "@agent/extensions/chat-claude.ts truncate all rea…" as Claude Opus. (2 [0m]8;;
|
||||||
|
[742] (w=114) [38;2;156;153;149mhistorical turns loaded)[39m [0m]8;;
|
||||||
|
[743] (w=0) [0m]8;;
|
||||||
|
[744] (w=44) [38;5;208m▌ [0m[38;5;208m[1m◆ CLAUDE CHAT MODE[0m [38;5;208m[1mOPUS[0m [38;5;130msession:c9492aa2[0m[0m]8;;
|
||||||
|
[745] (w=86) [38;5;208m▌ [0m[38;2;156;153;149mType to chat · /claude haiku|sonnet|opus · /claude-new · /claude-end · /claude-abort[39m[0m]8;;
|
||||||
|
[746] (w=114) [38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[0m]8;;
|
||||||
|
[747] (w=114) [7m [0m [0m]8;;
|
||||||
|
[748] (w=114) [38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[38;2;196;90;28m─[39m[0m]8;;
|
||||||
|
[749] (w=114) [38;2;156;153;149m~/dotfiles/pi/.pi[39m[38;2;156;153;149m · [39m[38;2;115;113;110m S [39m[38;2;30;155;82m[39m[38;2;156;153;149m⣀⣀⣀⣀⣀[39m [38;2;156;153;149m0%[39m[38;2;156;153;149m | [39m[38;2;115;113;110m W [39m[38;2;30;155;82m⣄[39m[38;2;156;153;149m⣀⣀⣀⣀[39m [38;2;156;153;149m3%[39m [38;2;156;153;149m⟳ 4d 7h[39m[38;2;156;153;149m | [39m[38;2;115;113;110mC [39m[38;2;30;155;82m[39m[38;2;156;153;149m⣀⣀⣀⣀⣀[39m [38;2;156;153;149m0%[39m[38;2;156;153;149m · [39m[38;2;156;153;149mminimax/minimax-m2.5:free[39m[38;2;156;153;149m · [39m[38;2;123;79;196m◆ Claud[0m...[0m[0m]8;;
|
||||||
779
pi/.pi/agent/shared/claude-stream.ts
Normal file
779
pi/.pi/agent/shared/claude-stream.ts
Normal file
@@ -0,0 +1,779 @@
|
|||||||
|
/**
|
||||||
|
* claude-stream — Shared types, rendering, and core spawn/stream logic
|
||||||
|
* for ask-claude and chat-claude pi extensions.
|
||||||
|
*
|
||||||
|
* Both extensions spawn `claude -p --output-format stream-json` and parse
|
||||||
|
* the same streaming protocol. This module provides:
|
||||||
|
* - Block types (ThinkingBlock, ToolBlock, TextBlock)
|
||||||
|
* - Rendering helpers (tool call lines, result boxes, usage formatting)
|
||||||
|
* - runClaude() — the core spawn + stream parser
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { diffLines } from "diff";
|
||||||
|
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Block types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ThinkingBlock {
|
||||||
|
type: "thinking";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolBlock {
|
||||||
|
type: "tool";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
inputJson: string;
|
||||||
|
editContext?: { before: string[]; after: string[]; startLine: number };
|
||||||
|
result?: { text: string; isError: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextBlock {
|
||||||
|
type: "text";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StreamBlock = ThinkingBlock | ToolBlock | TextBlock;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Details interface (stored in tool result, drives rendering)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ClaudeDetails {
|
||||||
|
label: string;
|
||||||
|
done: boolean;
|
||||||
|
blocks: StreamBlock[];
|
||||||
|
finalText: string;
|
||||||
|
sessionId?: string;
|
||||||
|
isResume?: boolean;
|
||||||
|
costUsd?: number;
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Rendering helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function shortenPath(p: string): string {
|
||||||
|
const home = process.env.HOME ?? "";
|
||||||
|
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUsage(d: ClaudeDetails): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (d.inputTokens) parts.push(`↑${d.inputTokens}`);
|
||||||
|
if (d.outputTokens) parts.push(`↓${d.outputTokens}`);
|
||||||
|
if (d.cacheReadTokens) parts.push(`R${d.cacheReadTokens}`);
|
||||||
|
if (d.cacheWriteTokens) parts.push(`W${d.cacheWriteTokens}`);
|
||||||
|
if (d.costUsd) parts.push(d.costUsd < 0.001 ? `$${(d.costUsd * 1000).toFixed(2)}m` : `$${d.costUsd.toFixed(4)}`);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Theme = {
|
||||||
|
fg: (c: any, t: string) => string;
|
||||||
|
bg: (c: any, t: string) => string;
|
||||||
|
bold: (t: string) => string;
|
||||||
|
italic: (t: string) => string;
|
||||||
|
dim?: (t: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderToolCallLine(block: ToolBlock, theme: Theme): string {
|
||||||
|
let input: Record<string, unknown> = {};
|
||||||
|
try { input = JSON.parse(block.inputJson); } catch { /* ok */ }
|
||||||
|
|
||||||
|
switch (block.name.toLowerCase()) {
|
||||||
|
case "read": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
const offset = Number(input.offset ?? 1);
|
||||||
|
const limit = input.limit != null ? Number(input.limit) : undefined;
|
||||||
|
const range = limit != null ? `:${offset}-${offset + limit - 1}` : "";
|
||||||
|
return theme.fg("muted", "read ") + theme.fg("accent", path) + theme.fg("warning", range);
|
||||||
|
}
|
||||||
|
case "bash": {
|
||||||
|
const cmd = String(input.command ?? "").replace(/\n/g, " ↵ ");
|
||||||
|
return theme.fg("muted", "$ ") + theme.fg("toolOutput", cmd);
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
return theme.fg("muted", "edit ") + theme.fg("accent", path);
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
const path = shortenPath(String(input.file_path ?? input.path ?? ""));
|
||||||
|
const lines = String(input.content ?? "").split("\n").length;
|
||||||
|
return theme.fg("muted", "write ") + theme.fg("accent", path) + theme.fg("dim", ` (${lines} lines)`);
|
||||||
|
}
|
||||||
|
case "glob": {
|
||||||
|
const pat = String(input.pattern ?? "");
|
||||||
|
const path = input.path ? shortenPath(String(input.path)) : ".";
|
||||||
|
return theme.fg("muted", "glob ") + theme.fg("accent", pat) + theme.fg("dim", ` in ${path}`);
|
||||||
|
}
|
||||||
|
case "grep": {
|
||||||
|
const pat = String(input.pattern ?? "");
|
||||||
|
const path = input.path ? shortenPath(String(input.path)) : ".";
|
||||||
|
return theme.fg("muted", "grep ") + theme.fg("accent", `"${pat}"`) + theme.fg("dim", ` in ${path}`);
|
||||||
|
}
|
||||||
|
case "mcp__pi__ask": {
|
||||||
|
// Surfaces from the pi-ask-bridge MCP server. input is
|
||||||
|
// { questions: [{ id, question, options[], multi?, recommended? }, ...] }
|
||||||
|
// Show the first question text inline; if there are more, append a count.
|
||||||
|
const qs = Array.isArray(input.questions) ? (input.questions as any[]) : [];
|
||||||
|
const first = qs[0];
|
||||||
|
const head = first?.question ? String(first.question) : "(empty)";
|
||||||
|
const more = qs.length > 1 ? ` (+${qs.length - 1} more)` : "";
|
||||||
|
const tag = first?.id ? ` [${first.id}]` : "";
|
||||||
|
return theme.fg("muted", "ask ") + theme.fg("accent", head) + theme.fg("dim", tag + more);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const desc = typeof input.description === "string" ? input.description
|
||||||
|
: typeof input.prompt === "string" ? input.prompt.split("\n")[0]
|
||||||
|
: block.inputJson;
|
||||||
|
return theme.fg("toolTitle", block.name) + theme.fg("dim", " " + desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderToolResultBox(block: ToolBlock, theme: Theme): Text {
|
||||||
|
if (!block.result) return new Text("", 0, 0);
|
||||||
|
|
||||||
|
let input: Record<string, unknown> = {};
|
||||||
|
try { input = JSON.parse(block.inputJson); } catch { /* ok */ }
|
||||||
|
|
||||||
|
// Strip / restyle synthetic envelopes like <tool_use_error> and
|
||||||
|
// <system_reminder> before any tool-specific parsing.
|
||||||
|
const text = transformSpecialTags(block.result.text);
|
||||||
|
const { isError } = block.result;
|
||||||
|
|
||||||
|
switch (block.name.toLowerCase()) {
|
||||||
|
case "read": {
|
||||||
|
const rawLines = text.split("\n").filter((l) => l.length > 0);
|
||||||
|
const parsed = rawLines.map((l) => {
|
||||||
|
const tab = l.indexOf("\t");
|
||||||
|
return tab >= 0 ? { num: l.slice(0, tab), content: l.slice(tab + 1) } : { num: "", content: l };
|
||||||
|
});
|
||||||
|
const maxNumLen = parsed.reduce((m, l) => Math.max(m, l.num.length), 0);
|
||||||
|
return new Text(
|
||||||
|
parsed.map((l) => theme.fg("dim", l.num.padStart(maxNumLen)) + " " + l.content).join("\n"),
|
||||||
|
0, 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
if (isError) return new Text(text, 0, 0);
|
||||||
|
const oldStr = String(input.old_string ?? input.oldText ?? "");
|
||||||
|
const newStr = String(input.new_string ?? input.newText ?? "");
|
||||||
|
if (!oldStr && !newStr) return new Text(text.slice(0, 200), 0, 0);
|
||||||
|
const oldLines = oldStr === "" ? [] : oldStr.split("\n");
|
||||||
|
const newLines = newStr === "" ? [] : newStr.split("\n");
|
||||||
|
const ctx = block.editContext;
|
||||||
|
const startLine = ctx?.startLine ?? 1;
|
||||||
|
const header = theme.fg("dim", `@@ -${startLine},${oldLines.length} +${startLine},${newLines.length} @@`);
|
||||||
|
const diff: string[] = [header];
|
||||||
|
for (const l of ctx?.before ?? []) diff.push(theme.fg("dim", " " + l));
|
||||||
|
for (const l of oldLines) diff.push(theme.fg("toolDiffRemoved", "-" + l));
|
||||||
|
for (const l of newLines) diff.push(theme.fg("toolDiffAdded", "+" + l));
|
||||||
|
for (const l of ctx?.after ?? []) diff.push(theme.fg("dim", " " + l));
|
||||||
|
return new Text(diff.join("\n"), 0, 0);
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
if (isError) return new Text(text, 0, 0);
|
||||||
|
const lines = String(input.content ?? "").split("\n");
|
||||||
|
const numWidth = String(lines.length).length;
|
||||||
|
return new Text(
|
||||||
|
lines.map((l, i) => theme.fg("dim", String(i + 1).padStart(numWidth)) + " " + l).join("\n"),
|
||||||
|
0, 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "bash":
|
||||||
|
return new Text(text.trimEnd(), 0, 0);
|
||||||
|
case "mcp__pi__ask": {
|
||||||
|
// The pi-ask-mcp server returns a JSON array of QuestionResults.
|
||||||
|
// Pretty-print as one "id → answer" line per question instead of
|
||||||
|
// dumping raw JSON into the result banner.
|
||||||
|
if (isError) return new Text(text.trim(), 0, 0);
|
||||||
|
let parsed: any;
|
||||||
|
try { parsed = JSON.parse(text); } catch { return new Text(text.trim(), 0, 0); }
|
||||||
|
if (!Array.isArray(parsed)) return new Text(text.trim(), 0, 0);
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const r of parsed) {
|
||||||
|
const id = theme.fg("accent", String(r?.id ?? "?"));
|
||||||
|
const opts = Array.isArray(r?.selectedOptions) ? r.selectedOptions : [];
|
||||||
|
const custom = r?.customInput ? String(r.customInput) : "";
|
||||||
|
const arrow = theme.fg("dim", " → ");
|
||||||
|
let answer: string;
|
||||||
|
if (opts.length === 0 && !custom) {
|
||||||
|
answer = theme.fg("warning", "(cancelled)");
|
||||||
|
} else if (opts.length > 0 && custom) {
|
||||||
|
answer = theme.fg("toolOutput", `[${opts.join(", ")}] + Other: "${custom}"`);
|
||||||
|
} else if (custom) {
|
||||||
|
answer = theme.fg("toolOutput", `Other: "${custom}"`);
|
||||||
|
} else {
|
||||||
|
answer = theme.fg("toolOutput", opts.length === 1 ? opts[0] : `[${opts.join(", ")}]`);
|
||||||
|
}
|
||||||
|
lines.push(id + arrow + answer);
|
||||||
|
}
|
||||||
|
return new Text(lines.join("\n"), 0, 0);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return new Text(text.trim(), 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip ANSI SGR sequences so we can re-style plain text. */
|
||||||
|
function stripAnsi(s: string): string {
|
||||||
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Special-tag transform — used for tool-result text that embeds synthetic
|
||||||
|
// envelopes like <tool_use_error> or <system_reminder>.
|
||||||
|
//
|
||||||
|
// IMPORTANT: all wrappers use PARTIAL resets (`\x1b[22;23;24;39m` — reset
|
||||||
|
// bold/dim/italic/underline and foreground only). If we emitted \x1b[0m in
|
||||||
|
// the middle of a line, it would also wipe any background that Box.bgFn
|
||||||
|
// painted around us, producing a staircase-shaped hole where the padding
|
||||||
|
// loses its colour. Partial resets leave the background intact.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const SGR_FG_RESET = "\x1b[22;23;24;39m";
|
||||||
|
const SGR_BOLD_WHITE = "\x1b[1;97m";
|
||||||
|
const SGR_BOLD_RED = "\x1b[1;91m";
|
||||||
|
const SGR_DIM_GREY = "\x1b[2;90m";
|
||||||
|
|
||||||
|
function transformSpecialTags(text: string): string {
|
||||||
|
// <tool_use_error>message</tool_use_error>
|
||||||
|
// → strip tags, display message in bright bold red
|
||||||
|
text = text.replace(
|
||||||
|
/<tool_use_error>\s*([\s\S]*?)\s*<\/tool_use_error>/g,
|
||||||
|
(_, inner: string) => SGR_BOLD_RED + inner + SGR_FG_RESET,
|
||||||
|
);
|
||||||
|
// <system_reminder> or <system-reminder>
|
||||||
|
// → strip tags, collapse whitespace to one line, truncate at ~100 chars
|
||||||
|
// with a trailing "...", render in dim grey
|
||||||
|
text = text.replace(
|
||||||
|
/<system[-_]reminder>\s*([\s\S]*?)\s*<\/system[-_]reminder>/g,
|
||||||
|
(_, inner: string) => {
|
||||||
|
const oneLine = inner.replace(/\s+/g, " ").trim();
|
||||||
|
const MAX = 100;
|
||||||
|
const snippet = oneLine.length > MAX ? oneLine.slice(0, MAX).trimEnd() : oneLine;
|
||||||
|
return SGR_DIM_GREY + snippet + "..." + SGR_FG_RESET;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a tool block as two stacked banners:
|
||||||
|
* 1) header — bold-white title on an ORANGE background (matches the
|
||||||
|
* chat-claude outer border colour).
|
||||||
|
* 2) result body (if any) — on the theme's pending / success / error bg.
|
||||||
|
*
|
||||||
|
* The header deliberately uses a different background from the result, so
|
||||||
|
* the "staircase" where the banners have different widths is an intentional
|
||||||
|
* stylistic cue, not a rendering glitch.
|
||||||
|
*/
|
||||||
|
const ORANGE_BG_FN = (s: string) => "\x1b[48;5;130m" + s + "\x1b[0m";
|
||||||
|
|
||||||
|
export function renderToolBlock(block: ToolBlock, theme: Theme): Container {
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
// ---- Header banner (orange) ----
|
||||||
|
// Strip per-segment colours from renderToolCallLine so the title renders
|
||||||
|
// uniformly bold-white over the orange background. Partial-reset at the
|
||||||
|
// end keeps the orange bg alive for the trailing padding.
|
||||||
|
const headerText = SGR_BOLD_WHITE + stripAnsi(renderToolCallLine(block, theme)) + SGR_FG_RESET;
|
||||||
|
c.addChild(new Text(headerText, 2, 0, ORANGE_BG_FN));
|
||||||
|
|
||||||
|
// ---- Result body ----
|
||||||
|
if (block.result !== undefined) {
|
||||||
|
const bgFn = block.result.isError
|
||||||
|
? (s: string) => theme.bg("toolErrorBg", s)
|
||||||
|
: (s: string) => theme.bg("toolSuccessBg", s);
|
||||||
|
const box = new Box(2, 0, bgFn);
|
||||||
|
box.addChild(renderToolResultBox(block, theme));
|
||||||
|
c.addChild(box);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a unified diff string for an edit tool call. */
|
||||||
|
export function buildEditDiff(oldStr: string, newStr: string, contextLines = 3): string {
|
||||||
|
const changes = diffLines(oldStr, newStr);
|
||||||
|
if (changes.length === 1 && (changes[0].added || changes[0].removed)) {
|
||||||
|
const oldCount = (oldStr ? oldStr.split("\n").length : 0);
|
||||||
|
const newCount = (newStr ? newStr.split("\n").length : 0);
|
||||||
|
const lines: string[] = [
|
||||||
|
`@@ -1,${oldCount} +1,${newCount} @@`,
|
||||||
|
...oldStr.split("\n").map((l) => "-" + l),
|
||||||
|
...newStr.split("\n").map((l) => "+" + l),
|
||||||
|
];
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctxLines: string[] = oldStr.split("\n");
|
||||||
|
const hunks: { start: number; lines: string[] }[] = [];
|
||||||
|
let currentHunk: { start: number; lines: string[] } | null = null;
|
||||||
|
let lineIdx = 0;
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
const lines = change.value.endsWith("\n") ? change.value.slice(0, -1).split("\n") : change.value.split("\n");
|
||||||
|
if (change.added) {
|
||||||
|
if (!currentHunk) currentHunk = { start: lineIdx, lines: [] };
|
||||||
|
for (const l of lines) currentHunk.lines.push("+" + l);
|
||||||
|
} else if (change.removed) {
|
||||||
|
if (!currentHunk) currentHunk = { start: lineIdx, lines: [] };
|
||||||
|
for (const l of lines) currentHunk.lines.push("-" + l);
|
||||||
|
lineIdx += lines.length;
|
||||||
|
} else {
|
||||||
|
for (const l of lines) {
|
||||||
|
if (!currentHunk) { lineIdx++; continue; }
|
||||||
|
currentHunk.lines.push(" " + l);
|
||||||
|
if (currentHunk.lines.length >= contextLines * 2 + 1) {
|
||||||
|
hunks.push(currentHunk);
|
||||||
|
currentHunk = null;
|
||||||
|
}
|
||||||
|
lineIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentHunk) hunks.push(currentHunk);
|
||||||
|
|
||||||
|
if (hunks.length === 0) return "(no changes)";
|
||||||
|
|
||||||
|
const merged: { start: number; lines: string[] }[] = [];
|
||||||
|
for (const h of hunks) {
|
||||||
|
if (merged.length === 0 || h.start <= merged[merged.length - 1].start + merged[merged.length - 1].lines.length) {
|
||||||
|
if (merged.length > 0) {
|
||||||
|
const prev = merged[merged.length - 1];
|
||||||
|
prev.lines.push(...h.lines.slice(Math.max(0, prev.lines.length - (h.start - prev.start))));
|
||||||
|
} else {
|
||||||
|
merged.push({ ...h, lines: [...h.lines] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.push({ ...h, lines: [...h.lines] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultLines: string[] = [];
|
||||||
|
for (const h of merged) {
|
||||||
|
const removed = h.lines.filter((l) => l.startsWith("-")).length;
|
||||||
|
const added = h.lines.filter((l) => l.startsWith("+")).length;
|
||||||
|
const total = h.lines.length;
|
||||||
|
resultLines.push(`@@ -${h.start},${removed + (total - removed - added)} +${h.start},${added + (total - removed - added)} @@`);
|
||||||
|
resultLines.push(...h.lines);
|
||||||
|
}
|
||||||
|
return resultLines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Error helpers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const ANTHROPIC_ERROR_MAP: [RegExp, string][] = [
|
||||||
|
[/rate_limit/i, "Rate limited by Anthropic — please try again in a few seconds."],
|
||||||
|
[/invalid_api_key/i, "Invalid Anthropic API key — check your ANTHROPIC_API_KEY."],
|
||||||
|
[/permission_error/i, "Permission denied — check your Anthropic account and API key."],
|
||||||
|
[/not_found.*model/i, "Model not found — the specified model is not available."],
|
||||||
|
[/context_length/i, "Context window exceeded — the input is too large for this model."],
|
||||||
|
[/overloaded/i, "Anthropic is overloaded — please retry shortly."],
|
||||||
|
[/billing/i, "Billing issue — check your Anthropic account status."],
|
||||||
|
[/invalid_request/i, "Invalid request — the prompt or parameters may be malformed."],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function formatAnthropicError(raw: string, exitCode?: number): string {
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(raw)) return msg;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw.trim());
|
||||||
|
if (parsed.type === "error" && parsed.error?.message) {
|
||||||
|
const em = parsed.error.message;
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(em)) return msg;
|
||||||
|
}
|
||||||
|
return em;
|
||||||
|
}
|
||||||
|
} catch { /* not JSON */ }
|
||||||
|
for (const line of raw.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("{")) continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (parsed.type === "error" && parsed.error?.message) {
|
||||||
|
const em = parsed.error.message;
|
||||||
|
for (const [re, msg] of ANTHROPIC_ERROR_MAP) {
|
||||||
|
if (re.test(em)) return msg;
|
||||||
|
}
|
||||||
|
return em;
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
}
|
||||||
|
if (raw.trim()) return raw.trim().slice(0, 500);
|
||||||
|
return `claude process exited with code ${exitCode ?? "unknown"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Core: spawn claude CLI and stream all output
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface RunClaudeOptions {
|
||||||
|
model?: string; // default: "sonnet"
|
||||||
|
agent?: string; // --agent <name> (from ~/.claude/agents/)
|
||||||
|
sessionId?: string; // --resume <id> (multi-turn)
|
||||||
|
noSessionPersistence?: boolean; // --no-session-persistence (ask-claude default)
|
||||||
|
enrichEditDiffs?: boolean; // enrich edit tool results with unified diffs
|
||||||
|
mcpConfigPath?: string; // --mcp-config <path> (e.g. pi-ask-bridge)
|
||||||
|
disallowedTools?: string[]; // --disallowed-tools (e.g. ["AskUserQuestion"])
|
||||||
|
// Extended-thinking effort level ("low"|"medium"|"high"|"xhigh"|"max").
|
||||||
|
// REQUIRED for thinking blocks to appear: in `-p` / stream-json mode the
|
||||||
|
// CLI does NOT honour the user's interactive `defaultThinkingLevel`
|
||||||
|
// setting — thinking_delta events are only emitted when `--effort` is
|
||||||
|
// passed explicitly. Pass "off" to leave the flag off entirely.
|
||||||
|
effort?: string;
|
||||||
|
extraEnv?: NodeJS.ProcessEnv; // merged on top of process.env for the child
|
||||||
|
cwd: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
timeoutMs?: number; // default: 15 min
|
||||||
|
onUpdate: (partial: { blocks: StreamBlock[]; finalText: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunClaudeResult {
|
||||||
|
finalText: string;
|
||||||
|
blocks: StreamBlock[];
|
||||||
|
sessionId: string;
|
||||||
|
costUsd: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
cacheWriteTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runClaude(prompt: string, opts: RunClaudeOptions): Promise<RunClaudeResult> {
|
||||||
|
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const args = ["-p", "--output-format", "stream-json", "--include-partial-messages"];
|
||||||
|
|
||||||
|
// Session mode: resume takes priority, then agent, then plain model
|
||||||
|
if (opts.sessionId) {
|
||||||
|
args.push("--resume", opts.sessionId);
|
||||||
|
} else if (opts.agent) {
|
||||||
|
args.push("--agent", opts.agent);
|
||||||
|
if (opts.model) args.push("--model", opts.model);
|
||||||
|
if (opts.noSessionPersistence) args.push("--no-session-persistence");
|
||||||
|
} else {
|
||||||
|
args.push("--model", opts.model ?? "sonnet");
|
||||||
|
if (opts.noSessionPersistence) args.push("--no-session-persistence");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended-thinking effort — must be passed explicitly in -p mode;
|
||||||
|
// the interactive `defaultThinkingLevel` setting does NOT apply here.
|
||||||
|
// Callers pass "off" to suppress the flag (e.g. ask-claude where
|
||||||
|
// raw speed matters more than thought traces).
|
||||||
|
if (opts.effort && opts.effort !== "off") {
|
||||||
|
args.push("--effort", opts.effort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP config (e.g. pi-ask-bridge for routing AskUserQuestion-style
|
||||||
|
// requests through pi's native UI). Additive — does NOT pass
|
||||||
|
// --strict-mcp-config, so the user's other configured MCP servers
|
||||||
|
// (exa, sentry, …) remain available to Claude.
|
||||||
|
if (opts.mcpConfigPath) {
|
||||||
|
args.push("--mcp-config", opts.mcpConfigPath);
|
||||||
|
}
|
||||||
|
// Tool denylist — typically ["AskUserQuestion"] when the MCP
|
||||||
|
// server is providing a replacement.
|
||||||
|
if (opts.disallowedTools?.length) {
|
||||||
|
args.push("--disallowed-tools", opts.disallowedTools.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = spawn("claude", args, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
cwd: opts.cwd,
|
||||||
|
env: opts.extraEnv ? { ...process.env, ...opts.extraEnv } : process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
proc.stdin.write(prompt, "utf8");
|
||||||
|
proc.stdin.end();
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as NodeJS.ErrnoException).code !== "EPIPE") reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = "";
|
||||||
|
const blocks: StreamBlock[] = [];
|
||||||
|
const pendingTools = new Map<number, { name: string; inputJson: string; id: string }>();
|
||||||
|
|
||||||
|
let sessionId = "";
|
||||||
|
let costUsd = 0, inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
|
||||||
|
|
||||||
|
const getOrCreateTextBlock = (): TextBlock => {
|
||||||
|
const last = blocks[blocks.length - 1];
|
||||||
|
if (last?.type === "text") return last;
|
||||||
|
const b: TextBlock = { type: "text", text: "" };
|
||||||
|
blocks.push(b);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrCreateThinkingBlock = (): ThinkingBlock => {
|
||||||
|
const last = blocks[blocks.length - 1];
|
||||||
|
if (last?.type === "thinking") return last;
|
||||||
|
const b: ThinkingBlock = { type: "thinking", text: "" };
|
||||||
|
blocks.push(b);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = () => {
|
||||||
|
const finalText = blocks.filter((b): b is TextBlock => b.type === "text").map((b) => b.text).join("");
|
||||||
|
opts.onUpdate({ blocks: [...blocks], finalText });
|
||||||
|
};
|
||||||
|
|
||||||
|
const processLine = (line: string) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let event: any;
|
||||||
|
try { event = JSON.parse(line); } catch { return; }
|
||||||
|
|
||||||
|
if (event.type === "stream_event") {
|
||||||
|
const e = event.event as any;
|
||||||
|
if (!e) return;
|
||||||
|
|
||||||
|
if (e.type === "content_block_start") {
|
||||||
|
const cb = e.content_block;
|
||||||
|
if (cb?.type === "tool_use") {
|
||||||
|
pendingTools.set(e.index as number, { name: cb.name, inputJson: "", id: cb.id });
|
||||||
|
}
|
||||||
|
} else if (e.type === "content_block_delta") {
|
||||||
|
const d = e.delta as any;
|
||||||
|
if (d?.type === "text_delta") {
|
||||||
|
getOrCreateTextBlock().text += d.text as string;
|
||||||
|
emit();
|
||||||
|
} else if (d?.type === "thinking_delta") {
|
||||||
|
getOrCreateThinkingBlock().text += d.thinking as string;
|
||||||
|
emit();
|
||||||
|
} else if (d?.type === "input_json_delta") {
|
||||||
|
const tool = pendingTools.get(e.index as number);
|
||||||
|
if (tool) tool.inputJson += d.partial_json as string ?? "";
|
||||||
|
}
|
||||||
|
} else if (e.type === "content_block_stop") {
|
||||||
|
const tool = pendingTools.get(e.index as number);
|
||||||
|
if (tool) {
|
||||||
|
// Claude CLI's --include-partial-messages can emit an `assistant`
|
||||||
|
// event with the completed tool_use BEFORE content_block_stop
|
||||||
|
// arrives. That path already pushed a block with the same id;
|
||||||
|
// update it in place instead of pushing a duplicate.
|
||||||
|
const existing = blocks.find(
|
||||||
|
(b): b is ToolBlock => b.type === "tool" && b.id === tool.id,
|
||||||
|
);
|
||||||
|
const target: ToolBlock = existing ?? {
|
||||||
|
type: "tool", id: tool.id, name: tool.name, inputJson: tool.inputJson,
|
||||||
|
};
|
||||||
|
if (existing) {
|
||||||
|
// Prefer the streamed inputJson (it's been accumulating and
|
||||||
|
// matches what claude-code actually executed).
|
||||||
|
existing.inputJson = tool.inputJson;
|
||||||
|
}
|
||||||
|
// For edit tools, read file context before the edit executes
|
||||||
|
if (tool.name.toLowerCase() === "edit" && !target.editContext) {
|
||||||
|
try {
|
||||||
|
const inp = JSON.parse(tool.inputJson) as Record<string, unknown>;
|
||||||
|
const fp = String(inp.file_path ?? inp.path ?? "");
|
||||||
|
const oldStr = String(inp.old_string ?? inp.oldText ?? "");
|
||||||
|
if (fp && oldStr) {
|
||||||
|
const fileLines = readFileSync(fp, "utf8").split("\n");
|
||||||
|
const oldLines = oldStr.split("\n");
|
||||||
|
let startIdx = -1;
|
||||||
|
outer: for (let i = 0; i <= fileLines.length - oldLines.length; i++) {
|
||||||
|
for (let j = 0; j < oldLines.length; j++) {
|
||||||
|
if (fileLines[i + j] !== oldLines[j]) continue outer;
|
||||||
|
}
|
||||||
|
startIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (startIdx >= 0) {
|
||||||
|
target.editContext = {
|
||||||
|
before: fileLines.slice(Math.max(0, startIdx - 3), startIdx),
|
||||||
|
after: fileLines.slice(startIdx + oldLines.length, startIdx + oldLines.length + 3),
|
||||||
|
startLine: startIdx + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!existing) blocks.push(target);
|
||||||
|
pendingTools.delete(e.index as number);
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
} else if (e.type === "message_delta") {
|
||||||
|
const u = e.usage as any;
|
||||||
|
if (u) {
|
||||||
|
inputTokens = u.input_tokens ?? inputTokens;
|
||||||
|
outputTokens = u.output_tokens ?? outputTokens;
|
||||||
|
cacheReadTokens = u.cache_read_input_tokens ?? cacheReadTokens;
|
||||||
|
cacheWriteTokens = u.cache_creation_input_tokens ?? cacheWriteTokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.type === "user") {
|
||||||
|
for (const c of (event.message?.content ?? []) as any[]) {
|
||||||
|
if (c.type !== "tool_result") continue;
|
||||||
|
// tool_result.content may be a plain string (typical for bash/read
|
||||||
|
// output) or an array of {type,text}/{type,image} blocks. Handle both.
|
||||||
|
let text: string;
|
||||||
|
if (typeof c.content === "string") {
|
||||||
|
text = c.content;
|
||||||
|
} else if (Array.isArray(c.content)) {
|
||||||
|
text = (c.content as any[])
|
||||||
|
.filter((x) => x.type === "text")
|
||||||
|
.map((x) => x.text as string)
|
||||||
|
.join("\n");
|
||||||
|
} else {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
const toolId = c.tool_use_id as string;
|
||||||
|
const toolBlock = blocks.findLast((b): b is ToolBlock => b.type === "tool" && b.id === toolId);
|
||||||
|
if (toolBlock && toolBlock.name.toLowerCase() === "edit" && !c.is_error && opts.enrichEditDiffs) {
|
||||||
|
try {
|
||||||
|
const inp = JSON.parse(toolBlock.inputJson) as Record<string, unknown>;
|
||||||
|
const oldStr = String(inp.old_string ?? inp.oldText ?? "");
|
||||||
|
const newStr = String(inp.new_string ?? inp.newText ?? "");
|
||||||
|
if (oldStr || newStr) {
|
||||||
|
const diff = buildEditDiff(oldStr, newStr);
|
||||||
|
text = `${text}\n\n--- edit diff ---\n${diff}\n--- end diff ---`;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (toolBlock) toolBlock.result = { text, isError: c.is_error === true };
|
||||||
|
emit();
|
||||||
|
}
|
||||||
|
} else if (event.type === "assistant") {
|
||||||
|
const content: any[] = (event as any).message?.content ?? [];
|
||||||
|
for (const cb of content) {
|
||||||
|
if (cb.type !== "tool_use") continue;
|
||||||
|
const exists = blocks.some((b): b is ToolBlock => b.type === "tool" && b.id === cb.id);
|
||||||
|
if (!exists) blocks.push({ type: "tool", id: cb.id, name: cb.name, inputJson: JSON.stringify(cb.input ?? {}) });
|
||||||
|
}
|
||||||
|
emit();
|
||||||
|
} else if (event.type === "result") {
|
||||||
|
sessionId = event.session_id ?? "";
|
||||||
|
costUsd = event.total_cost_usd ?? 0;
|
||||||
|
const u = event.usage as any;
|
||||||
|
if (u) {
|
||||||
|
inputTokens = u.input_tokens ?? inputTokens;
|
||||||
|
outputTokens = u.output_tokens ?? outputTokens;
|
||||||
|
cacheReadTokens = u.cache_read_input_tokens ?? cacheReadTokens;
|
||||||
|
cacheWriteTokens = u.cache_creation_input_tokens ?? cacheWriteTokens;
|
||||||
|
}
|
||||||
|
} else if (event.type === "error") {
|
||||||
|
const errMsg = event.error?.message ?? event.message ?? JSON.stringify(event);
|
||||||
|
reject(new Error(formatAnthropicError(errMsg)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data: Buffer) => {
|
||||||
|
buffer += data.toString();
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
for (const line of lines) processLine(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderrOutput = "";
|
||||||
|
proc.stderr.on("data", (data: Buffer) => { stderrOutput += data.toString(); });
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (buffer.trim()) processLine(buffer);
|
||||||
|
const finalText = blocks.filter((b): b is TextBlock => b.type === "text").map((b) => b.text).join("");
|
||||||
|
if (code !== 0) {
|
||||||
|
if (timeoutFired) {
|
||||||
|
reject(new Error(`Claude timed out after ${timeoutMs / 1000}s — the process was killed. Consider using a simpler prompt or a faster model.`));
|
||||||
|
} else {
|
||||||
|
const errMsg = formatAnthropicError(stderrOutput.trim(), code);
|
||||||
|
const detail = finalText ? ` (partial output: ${finalText.slice(0, 100)})` : "";
|
||||||
|
reject(new Error(errMsg + detail));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ finalText, blocks, sessionId, costUsd, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens });
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
reject(new Error("Claude CLI not found — install it with `npm install -g @anthropic-ai/claude-code`"));
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.signal) {
|
||||||
|
const kill = () => {
|
||||||
|
try { if (!proc.killed) proc.kill("SIGTERM"); } catch { /* ok */ }
|
||||||
|
setTimeout(() => { try { if (!proc.killed) proc.kill("SIGKILL"); } catch { /* ok */ } }, 3000);
|
||||||
|
};
|
||||||
|
if (opts.signal.aborted) kill();
|
||||||
|
else opts.signal.addEventListener("abort", kill, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutFired = false;
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
timeoutFired = true;
|
||||||
|
try { proc.kill("SIGTERM"); } catch { /* ok */ }
|
||||||
|
setTimeout(() => { try { if (!proc.killed) proc.kill("SIGKILL"); } catch { /* ok */ } }, 3000);
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
proc.prependListener("close", () => { clearTimeout(timeoutId); });
|
||||||
|
proc.prependListener("error", () => { clearTimeout(timeoutId); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RenderResult factory — produces the common result rendering for both extensions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function renderClaudeResult(
|
||||||
|
result: { details?: ClaudeDetails; isPartial?: boolean },
|
||||||
|
theme: Theme,
|
||||||
|
opts?: { showSession?: boolean }
|
||||||
|
): Container | Text {
|
||||||
|
const d = result.details as ClaudeDetails | undefined;
|
||||||
|
if (!d) return new Text(theme.fg("muted", "…"), 0, 0);
|
||||||
|
|
||||||
|
const isDone = d.done && !result.isPartial;
|
||||||
|
const icon = isDone ? theme.fg("success", "✓ ") : theme.fg("warning", "⏳ ");
|
||||||
|
const resume = d.isResume ? theme.fg("dim", " ↩") : "";
|
||||||
|
const c = new Container();
|
||||||
|
|
||||||
|
c.addChild(new Text(icon + theme.fg("toolTitle", theme.bold(d.label)) + resume, 0, 0));
|
||||||
|
|
||||||
|
for (const block of d.blocks ?? []) {
|
||||||
|
if (block.type === "thinking" && block.text.trim()) {
|
||||||
|
c.addChild(new Text(theme.fg("dim", theme.italic(block.text.trimEnd())), 0, 0));
|
||||||
|
} else if (block.type === "tool") {
|
||||||
|
c.addChild(renderToolBlock(block, theme));
|
||||||
|
} else if (block.type === "text" && block.text.trim()) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
if (isDone) {
|
||||||
|
c.addChild(new Markdown(block.text.trim(), 0, 0, getMarkdownTheme()));
|
||||||
|
} else {
|
||||||
|
c.addChild(new Text(theme.fg("dim", block.text.trimEnd()), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone) {
|
||||||
|
const usageLine = formatUsage(d);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (usageLine) parts.push(usageLine);
|
||||||
|
if (opts?.showSession && d.sessionId) parts.push(`session:${d.sessionId.slice(0, 8)}`);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
c.addChild(new Spacer(1));
|
||||||
|
c.addChild(new Text(theme.fg("dim", parts.join(" ")), 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
201
pi/.pi/agent/shared/pi-ask-bridge.ts
Normal file
201
pi/.pi/agent/shared/pi-ask-bridge.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* pi-ask-bridge — Unix-domain socket server that pipes ask requests from
|
||||||
|
* the pi-ask-mcp subprocess (running inside Claude CLI inside chat-claude)
|
||||||
|
* into pi's native ask UI (askSingleQuestionWithInlineNote / askQuestionsWithTabs).
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
*
|
||||||
|
* pi process
|
||||||
|
* └── chat-claude extension
|
||||||
|
* ├── AskBridge (here) — listens on $PI_ASK_SOCKET
|
||||||
|
* └── claude -p ... --mcp-config <generated>
|
||||||
|
* └── pi-ask-mcp/server.js
|
||||||
|
* ↳ on tools/call ask → connects to $PI_ASK_SOCKET,
|
||||||
|
* sends question, awaits answer
|
||||||
|
*
|
||||||
|
* Lifecycle: start one bridge per chat-claude session; close on exit.
|
||||||
|
* Concurrency: pi's ui.custom overlay is modal, so asks are serialised
|
||||||
|
* across all open connections via an internal promise chain.
|
||||||
|
*
|
||||||
|
* Wire format (NDJSON, one message per line):
|
||||||
|
*
|
||||||
|
* request → { id, type: "ask", questions: [{id, question, options[], multi?, recommended?}, ...] }
|
||||||
|
* response ← { id, type: "result", results: [{id, selectedOptions[], customInput?}, ...] }
|
||||||
|
* error ← { id, type: "error", message: "…" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createServer, type Server as NetServer, type Socket } from "node:net";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { askSingleQuestionWithInlineNote } from "../extensions/pi-ask-tool/ask-inline-ui.js";
|
||||||
|
import { askQuestionsWithTabs } from "../extensions/pi-ask-tool/ask-tabs-ui.js";
|
||||||
|
import type { AskQuestion, AskSelection } from "../extensions/pi-ask-tool/ask-logic.js";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Public API
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface AskBridge {
|
||||||
|
/** Path to the generated --mcp-config JSON, suitable for `claude --mcp-config`. */
|
||||||
|
mcpConfigPath: string;
|
||||||
|
/** Absolute path to the underlying Unix socket (informational). */
|
||||||
|
socketPath: string;
|
||||||
|
/** How many ask requests this bridge has served so far. */
|
||||||
|
count(): number;
|
||||||
|
/** Stop accepting connections, remove socket + temp dir. Idempotent. */
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartAskBridgeOptions {
|
||||||
|
/** pi UI context (must come from an interactive session). */
|
||||||
|
ui: ExtensionUIContext;
|
||||||
|
/**
|
||||||
|
* Absolute path to extensions/pi-ask-mcp/server.js. Auto-derived from
|
||||||
|
* import.meta.url when omitted (assumes the conventional layout).
|
||||||
|
*/
|
||||||
|
mcpServerEntry?: string;
|
||||||
|
/**
|
||||||
|
* MCP server name surfaced in the tool prefix. Defaults to "pi", which
|
||||||
|
* makes the tool name `mcp__pi__ask` in Claude's tool stream.
|
||||||
|
*/
|
||||||
|
serverName?: string;
|
||||||
|
/** Optional notification fired whenever a new ask is served. */
|
||||||
|
onAsk?: (totalSoFar: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startAskBridge(opts: StartAskBridgeOptions): AskBridge {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "pi-ask-")); // 0700 perms
|
||||||
|
const sock = join(dir, "ask.sock");
|
||||||
|
let askCount = 0;
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
const server: NetServer = createServer((conn) =>
|
||||||
|
handleConnection(conn, opts, () => {
|
||||||
|
askCount += 1;
|
||||||
|
opts.onAsk?.(askCount);
|
||||||
|
return askCount;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
server.on("error", () => { /* socket disappeared, etc. — bridge is single-tenant, ignore */ });
|
||||||
|
server.listen(sock);
|
||||||
|
|
||||||
|
const mcpEntry = opts.mcpServerEntry ?? defaultMcpEntry();
|
||||||
|
const serverName = opts.serverName ?? "pi";
|
||||||
|
const cfgPath = join(dir, "mcp.json");
|
||||||
|
writeFileSync(cfgPath, JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
[serverName]: {
|
||||||
|
command: "node",
|
||||||
|
args: [mcpEntry],
|
||||||
|
env: { PI_ASK_SOCKET: sock },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
socketPath: sock,
|
||||||
|
mcpConfigPath: cfgPath,
|
||||||
|
count: () => askCount,
|
||||||
|
close: () => {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
try { server.close(); } catch { /* noop */ }
|
||||||
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Internals
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function defaultMcpEntry(): string {
|
||||||
|
// shared/pi-ask-bridge.ts → ../extensions/pi-ask-mcp/server.js
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
return join(here, "..", "extensions", "pi-ask-mcp", "server.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
// pi.ui.custom is modal — only one overlay can be on screen at a time.
|
||||||
|
// Serialise asks across ALL connections via this single promise chain.
|
||||||
|
let askChain: Promise<unknown> = Promise.resolve();
|
||||||
|
|
||||||
|
function handleConnection(
|
||||||
|
conn: Socket,
|
||||||
|
opts: StartAskBridgeOptions,
|
||||||
|
bumpCount: () => number,
|
||||||
|
) {
|
||||||
|
let buf = "";
|
||||||
|
conn.on("data", (data) => {
|
||||||
|
buf += data.toString();
|
||||||
|
let nl = buf.indexOf("\n");
|
||||||
|
while (nl >= 0) {
|
||||||
|
const line = buf.slice(0, nl);
|
||||||
|
buf = buf.slice(nl + 1);
|
||||||
|
handleLine(line, conn, opts, bumpCount);
|
||||||
|
nl = buf.indexOf("\n");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
conn.on("error", () => { /* peer might disappear if Claude is killed mid-flight */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLine(
|
||||||
|
line: string,
|
||||||
|
conn: Socket,
|
||||||
|
opts: StartAskBridgeOptions,
|
||||||
|
bumpCount: () => number,
|
||||||
|
) {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let msg: any;
|
||||||
|
try { msg = JSON.parse(line); } catch { return; }
|
||||||
|
if (msg.type !== "ask") return;
|
||||||
|
const id = String(msg.id ?? "");
|
||||||
|
const questions = Array.isArray(msg.questions) ? (msg.questions as AskQuestion[]) : [];
|
||||||
|
|
||||||
|
askChain = askChain.then(async () => {
|
||||||
|
bumpCount();
|
||||||
|
try {
|
||||||
|
const results = await askViaPiUI(opts.ui, questions);
|
||||||
|
writeReply(conn, { id, type: "result", results });
|
||||||
|
} catch (err) {
|
||||||
|
writeReply(conn, {
|
||||||
|
id,
|
||||||
|
type: "error",
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeReply(conn: Socket, msg: unknown) {
|
||||||
|
try { conn.write(JSON.stringify(msg) + "\n"); conn.end(); } catch { /* gone */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionResult {
|
||||||
|
id: string;
|
||||||
|
selectedOptions: string[];
|
||||||
|
customInput?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askViaPiUI(
|
||||||
|
ui: ExtensionUIContext,
|
||||||
|
questions: AskQuestion[],
|
||||||
|
): Promise<QuestionResult[]> {
|
||||||
|
if (questions.length === 0) return [];
|
||||||
|
|
||||||
|
if (questions.length === 1 && !questions[0].multi) {
|
||||||
|
const sel: AskSelection = await askSingleQuestionWithInlineNote(ui, questions[0]);
|
||||||
|
return [toResult(questions[0], sel)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = await askQuestionsWithTabs(ui, questions);
|
||||||
|
return questions.map((q, i) => toResult(q, tab.selections[i] ?? { selectedOptions: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toResult(q: AskQuestion, sel: AskSelection): QuestionResult {
|
||||||
|
const out: QuestionResult = { id: q.id, selectedOptions: [...sel.selectedOptions] };
|
||||||
|
if (sel.customInput) out.customInput = sel.customInput;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
110
pi/.pi/agent/skills/add-agent/SKILL.md
Normal file
110
pi/.pi/agent/skills/add-agent/SKILL.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: add-agent
|
||||||
|
description: "Add a new Claude agent definition to ~/.claude/agents/ and an accompanying skill to ~/.pi/agent/skills/. Use when creating new specialized agents."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add Agent
|
||||||
|
|
||||||
|
When the user wants to create a new specialized Claude agent:
|
||||||
|
|
||||||
|
## Agent File Convention
|
||||||
|
|
||||||
|
Create a `.md` file in `~/.claude/agents/` with this structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: agent_name
|
||||||
|
description: One-line description of what the agent does
|
||||||
|
tools: Read, Bash[, Edit, Write] # include only what's needed
|
||||||
|
model: sonnet | opus
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an [role]. [1-2 sentence description of purpose].
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
- read: Read file contents
|
||||||
|
- bash: Execute bash commands
|
||||||
|
- edit: Make surgical edits to files # if applicable
|
||||||
|
- write: Create or overwrite files # if applicable
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Use bash for file operations: prefer `rg` over grep, `fd` over find, glob patterns for batch file matching
|
||||||
|
- Use read to examine files [before editing] # adapt phrasing
|
||||||
|
- [Agent-specific guidelines]
|
||||||
|
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||||
|
- Be concise in your responses
|
||||||
|
- Show file paths clearly when working with files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
1. **`name`** in frontmatter must match what skills reference as `agent: "name"`
|
||||||
|
2. **`tools`** — only include tools the agent actually needs. Read-only agents use `Read, Bash`
|
||||||
|
3. **`model`** — `sonnet` for execution/review, `opus` for strategic/advisory work
|
||||||
|
4. **Always include** the "output plain text directly" guideline — agents without it tend to use `cat`/`echo` instead of responding directly
|
||||||
|
5. **Bash guideline** should read: `prefer \`rg\` over grep, \`fd\` over find, glob patterns for batch file matching`
|
||||||
|
6. **Bash tool description** should be: `Execute bash commands`
|
||||||
|
|
||||||
|
## Skill File Convention
|
||||||
|
|
||||||
|
Create `~/.pi/agent/skills/<skill-name>/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: skill-name
|
||||||
|
description: "What triggers this skill. Use when [condition]."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Title
|
||||||
|
|
||||||
|
When [trigger condition]:
|
||||||
|
|
||||||
|
## What to include in the prompt
|
||||||
|
|
||||||
|
1. **[Section 1]** — description
|
||||||
|
2. **[Section 2]** — description
|
||||||
|
...
|
||||||
|
|
||||||
|
The `agent_name` agent has `Tool1` and `Tool2` tools only — [what it can/can't do].
|
||||||
|
|
||||||
|
## How to call
|
||||||
|
|
||||||
|
\```
|
||||||
|
ask_claude({
|
||||||
|
agent: "agent_name",
|
||||||
|
question: "Specific instruction for the agent.",
|
||||||
|
prompt: `
|
||||||
|
## Section 1
|
||||||
|
[Template]
|
||||||
|
|
||||||
|
## Section 2
|
||||||
|
[Template]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
\```
|
||||||
|
|
||||||
|
## After the review
|
||||||
|
|
||||||
|
- [What to do with the agent's output]
|
||||||
|
- [How to summarize for the user]
|
||||||
|
- [When to loop back or escalate]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill Key Rules
|
||||||
|
|
||||||
|
1. **Skill `name`** should be kebab-case (e.g., `claude-debug`, not `claude_debug`)
|
||||||
|
2. **Description** must be in quotes if it contains special characters
|
||||||
|
3. **`agent:` in `ask_claude()`** must match the agent's `name:` exactly
|
||||||
|
4. **Include a "What to include" section** — gives the calling agent a template
|
||||||
|
5. **Include a "How to call" section** — with a concrete `ask_claude()` example
|
||||||
|
6. **Include an "After the review" section** — what to do with the output
|
||||||
|
|
||||||
|
## Existing Agents (for reference)
|
||||||
|
|
||||||
|
| Agent | Model | Tools | Purpose |
|
||||||
|
|-------|-------|-------|---------|
|
||||||
|
| `minimal` | sonnet | Read, Bash, Edit, Write | General coding |
|
||||||
|
| `code_review` | sonnet | Read, Bash, Edit, Write | Review & fix code |
|
||||||
|
| `plan_review` | opus | Read, Bash | Review plans |
|
||||||
|
| `debug` | sonnet | Read, Bash | Trace bugs |
|
||||||
|
| `oracle` | opus | Read, Bash | Strategic guidance |
|
||||||
153
pi/.pi/agent/skills/ask-claude/SKILL.md
Normal file
153
pi/.pi/agent/skills/ask-claude/SKILL.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
name: ask-claude
|
||||||
|
description: "Invoke Claude (any agent or model) for an opinion, review, or analysis. Use when the user asks you to ask a specific Claude about something specific — e.g. trade-offs of an approach, a focused review, or a second opinion."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ask Claude
|
||||||
|
|
||||||
|
Use `ask_claude` to consult Claude with any combination of agent, model, question, and prompt.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| `prompt` | yes | The content for Claude to reason about (context, code, plan, bug report, etc.) |
|
||||||
|
| `question` | no | What specifically to ask — prepended as a focused review directive |
|
||||||
|
| `agent` | no | Agent name from `~/.claude/agents/`. See **Available agents** below. |
|
||||||
|
| `model` | no | Override the model: `"opus"`, `"sonnet"`, `"haiku"` |
|
||||||
|
| `session_id` | no | Resume a prior conversation (returned in every response) |
|
||||||
|
|
||||||
|
If neither `agent` nor `model` is set, defaults to Claude Sonnet.
|
||||||
|
|
||||||
|
> **Multi-turn:** Every response includes a `session_id`. Pass it back in a subsequent `ask_claude()` call to continue the same conversation with the same agent/model.
|
||||||
|
|
||||||
|
## Available agents
|
||||||
|
|
||||||
|
See `~/.claude/agents/` for the full list. Common agents:
|
||||||
|
|
||||||
|
| Agent | Model | Tools | Use when |
|
||||||
|
|-------|-------|-------|----------|
|
||||||
|
| `plan_review` | Opus | Read, Bash | Reviewing plans for correctness, completeness, feasibility, and risk |
|
||||||
|
| `code_review` | Sonnet | Read, Bash, Edit, Write | Reviewing implementations; can apply fixes directly |
|
||||||
|
| `debug` | Sonnet | Read, Bash | Tracing bugs and root causes; will NOT apply fixes |
|
||||||
|
| `oracle` | Opus | Read, Bash | Hard problems, architectural decisions, when you're stuck |
|
||||||
|
|
||||||
|
Pick the agent that matches the task. If unsure, ask the user which agent to use, or use a raw `model=` instead of an agent.
|
||||||
|
|
||||||
|
## Common patterns
|
||||||
|
|
||||||
|
### Plan review
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "plan_review",
|
||||||
|
question: "Review for correctness, completeness, feasibility, and risk. Highlight missing steps or unclear requirements.",
|
||||||
|
prompt: `
|
||||||
|
## Project Context
|
||||||
|
[Key facts from CLAUDE.md]
|
||||||
|
|
||||||
|
## Codebase Exploration
|
||||||
|
[Modules/files you read]
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
[Your full plan, step by step]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
[Anything uncertain]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code review (with fix capability)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "code_review",
|
||||||
|
question: "Review for bugs, architectural issues, style, and correctness. Apply fixes for any issues you find.",
|
||||||
|
prompt: `
|
||||||
|
## Project Conventions
|
||||||
|
[Relevant context not in CLAUDE.md]
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
[Brief description]
|
||||||
|
|
||||||
|
## Plan That Was Followed
|
||||||
|
[The implementation plan]
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- path/to/file.ts — [what changed]
|
||||||
|
|
||||||
|
## Code to Review
|
||||||
|
[Paste key sections OR instruct the agent to read the files above]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "debug",
|
||||||
|
question: "Trace the root cause. Provide specific file paths, line numbers, and the exact code responsible. Suggest a fix but do not apply it.",
|
||||||
|
prompt: `
|
||||||
|
## The Issue
|
||||||
|
[What's happening vs. what should happen]
|
||||||
|
|
||||||
|
## Error Output
|
||||||
|
[Any logs, stack traces, or error messages]
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- path/to/file.ts — [why it's relevant]
|
||||||
|
|
||||||
|
## What I've Already Tried
|
||||||
|
[Hypotheses tested, things ruled out]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oracle (hard problems)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "oracle",
|
||||||
|
question: "Analyze this problem and provide guidance. Explain your reasoning and recommend the best path forward.",
|
||||||
|
prompt: `
|
||||||
|
## The Problem
|
||||||
|
[What you're trying to solve and why it's hard]
|
||||||
|
|
||||||
|
## Codebase Context
|
||||||
|
[Relevant files, modules, and patterns explored]
|
||||||
|
|
||||||
|
## Options Considered
|
||||||
|
[Approaches evaluated and their trade-offs]
|
||||||
|
|
||||||
|
## What I Need Clarified
|
||||||
|
[Specific question or decision point]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Free-form model question (no agent)
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
model: "opus",
|
||||||
|
question: "What are the trade-offs of this approach vs using a message queue?",
|
||||||
|
prompt: `We're considering polling a database table every 5 seconds for new jobs instead of a queue...`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuing a conversation
|
||||||
|
When Claude returns a `session_id` in its response, pass it back to continue the same conversation:
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "code_review",
|
||||||
|
question: "Apply the fixes you identified.",
|
||||||
|
session_id: "<session_id from prior response>",
|
||||||
|
prompt: "Please proceed with the fixes."
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## After the response
|
||||||
|
|
||||||
|
- Summarize Claude's key points for the user.
|
||||||
|
- If Claude raises blockers or important concerns, address them before proceeding.
|
||||||
|
- For plan reviews: if blockers or missing steps are flagged, update the plan and re-invoke if changes are significant.
|
||||||
|
- For code reviews: Claude (via `code_review`) may apply fixes directly — summarize what was changed.
|
||||||
|
- For debugging: apply the fix yourself or delegate to a coding agent.
|
||||||
|
- For oracle: proceed with the recommended approach, or loop back if more clarification is needed.
|
||||||
50
pi/.pi/agent/skills/implementor/SKILL.md
Normal file
50
pi/.pi/agent/skills/implementor/SKILL.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: implementor
|
||||||
|
description: "Delegate implementation of a fix or feature to the implementor agent. Use when you have a clear plan, bug fix, or feature to implement and want a focused agent to handle the full coding + build-verify cycle."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementor
|
||||||
|
|
||||||
|
When you need to implement a fix, feature, or refactoring with full build verification:
|
||||||
|
|
||||||
|
## What to include in the prompt
|
||||||
|
|
||||||
|
1. **Goal** — what needs to be built or fixed, in 1-2 sentences
|
||||||
|
2. **Context** — relevant files, functions, or modules (include the actual code or file paths)
|
||||||
|
3. **Plan** — step-by-step description of the changes to make (if you have one)
|
||||||
|
4. **Constraints** — any project conventions, patterns to follow, or things to avoid
|
||||||
|
5. **Verification** — how to confirm the implementation is correct (build command, test names, etc.)
|
||||||
|
|
||||||
|
The `implementor` agent has `Read`, `Bash`, `Edit`, and `Write` tools only — it can read code, run commands, and make changes. It cannot use semantic search or web search.
|
||||||
|
|
||||||
|
## How to call
|
||||||
|
|
||||||
|
```
|
||||||
|
ask_claude({
|
||||||
|
agent: "implementor",
|
||||||
|
question: "Implement [brief description of the fix or feature].",
|
||||||
|
prompt: `
|
||||||
|
## Goal
|
||||||
|
[What needs to be done]
|
||||||
|
|
||||||
|
## Context
|
||||||
|
[Relevant files and code snippets]
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
[Step-by-step changes, or "investigate and determine the best approach"]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
[Project conventions, patterns to follow, things to avoid]
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
[Build command, tests to run, how to confirm correctness]
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## After the review
|
||||||
|
|
||||||
|
- Check whether the agent reported successful build/verification
|
||||||
|
- If it failed, either re-invoke with the error details or fix the remaining issues directly
|
||||||
|
- Summarize for the user: what files changed, what was verified, and any remaining concerns
|
||||||
|
- For complex implementations, consider running `diagnostics` sub-agent or relevant tests afterward as a second pass
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||||
"name": "wezterm-sync-ba8a76f5",
|
"name": "wezterm-sync-9a35138e",
|
||||||
"vars": {
|
"vars": {
|
||||||
"bg": "#1c2433",
|
"bg": "#faf4ed",
|
||||||
"fg": "#afbbd2",
|
"fg": "#2a2a2a",
|
||||||
"accent": "#b78aff",
|
"accent": "#7b4fc4",
|
||||||
"accentAlt": "#ff955c",
|
"accentAlt": "#c45a1c",
|
||||||
"link": "#69c3ff",
|
"link": "#1a7db5",
|
||||||
"error": "#ff738a",
|
"error": "#d1344f",
|
||||||
"success": "#3cec85",
|
"success": "#1e9b52",
|
||||||
"warning": "#eacd61",
|
"warning": "#b8890f",
|
||||||
"muted": "#7c869a",
|
"muted": "#73716e",
|
||||||
"dim": "#5e687b",
|
"dim": "#9c9995",
|
||||||
"borderMuted": "#414a5b",
|
"borderMuted": "#c6c2bc",
|
||||||
"selectedBg": "#28303f",
|
"selectedBg": "#eee8e1",
|
||||||
"userMsgBg": "#242c3b",
|
"userMsgBg": "#f2ece5",
|
||||||
"toolPendingBg": "#212938",
|
"toolPendingBg": "#f5efe8",
|
||||||
"toolSuccessBg": "#203c3d",
|
"toolSuccessBg": "#e0e9da",
|
||||||
"toolErrorBg": "#372d3d",
|
"toolErrorBg": "#f5ddda",
|
||||||
"customMsgBg": "#282c43"
|
"customMsgBg": "#f0e7ea"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"accent": "accent",
|
"accent": "accent",
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
"bashMode": "success"
|
"bashMode": "success"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"pageBg": "#141c2b",
|
"pageBg": "#fffcf5",
|
||||||
"cardBg": "#1c2433",
|
"cardBg": "#faf4ed",
|
||||||
"infoBg": "#353839"
|
"infoBg": "#f2e7d2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"hide_thinking_block": true
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user