refactor: split dense lines in binary lookup and transcript replay

This commit is contained in:
Ivanko1608
2026-10-25 02:04:22 +04:00
parent 2b2cf79b9a
commit a86a4622fb
4 changed files with 81 additions and 15 deletions
+29 -5
View File
@@ -18,32 +18,51 @@ type SearchSpec = {
function getWindowsSearchSpec(env: BinaryLookup["env"], homeDir: string): SearchSpec {
const paths = path.win32;
const appData = env.APPDATA ?? paths.join(homeDir, "AppData", "Roaming");
return {
paths,
names: ["claude.exe", "claude.cmd"],
knownDirs: [paths.join(homeDir, ".local", "bin"), paths.join(homeDir, ".claude", "local"), env.npm_config_prefix ?? paths.join(appData, "npm")],
knownDirs: [
paths.join(homeDir, ".local", "bin"),
paths.join(homeDir, ".claude", "local"),
env.npm_config_prefix ?? paths.join(appData, "npm"),
],
};
}
function getPosixSearchSpec(env: BinaryLookup["env"], homeDir: string): SearchSpec {
const paths = path.posix;
const npmBin = env.npm_config_prefix ? paths.join(env.npm_config_prefix, "bin") : paths.join(homeDir, ".npm-global", "bin");
const npmBin = env.npm_config_prefix
? paths.join(env.npm_config_prefix, "bin")
: paths.join(homeDir, ".npm-global", "bin");
return {
paths,
names: ["claude"],
knownDirs: [paths.join(homeDir, ".local", "bin"), paths.join(homeDir, ".claude", "local"), npmBin, "/opt/homebrew/bin", "/usr/local/bin"],
knownDirs: [
paths.join(homeDir, ".local", "bin"),
paths.join(homeDir, ".claude", "local"),
npmBin,
"/opt/homebrew/bin",
"/usr/local/bin",
],
};
}
export function resolveClaudeBinary({ env, platform, homeDir, isExecutable }: BinaryLookup): string | null {
const { paths, names, knownDirs } = platform === "win32" ? getWindowsSearchSpec(env, homeDir) : getPosixSearchSpec(env, homeDir);
const { paths, names, knownDirs } = platform === "win32"
? getWindowsSearchSpec(env, homeDir)
: getPosixSearchSpec(env, homeDir);
const pathDirs = (env.PATH ?? "").split(paths.delimiter).filter(Boolean);
for (const name of names) {
for (const dir of [...pathDirs, ...knownDirs]) {
const candidate = paths.join(dir, name);
if (isExecutable(candidate)) return candidate;
}
}
return null;
}
@@ -57,5 +76,10 @@ function isExecutableFile(filePath: string): boolean {
}
export function getClaudeBinary(): string | null {
return resolveClaudeBinary({ env: process.env, platform: process.platform, homeDir: os.homedir(), isExecutable: isExecutableFile });
return resolveClaudeBinary({
env: process.env,
platform: process.platform,
homeDir: os.homedir(),
isExecutable: isExecutableFile,
});
}
+30 -6
View File
@@ -1,7 +1,9 @@
import type { ImageContent, Message, TextContent, Tool } from "@earendil-works/pi-ai";
import { getCurrentSystemPrompt, getCurrentTools } from "@earendil-works/pi-ai/utils/transcript";
export type ClaudeContentBlock = { type: "text"; text: string } | { type: "image"; source: { type: "base64"; media_type: string; data: string } };
export type ClaudeContentBlock =
| { type: "text"; text: string }
| { type: "image"; source: { type: "base64"; media_type: string; data: string } };
export type ClaudeInput = {
systemPrompt: string;
@@ -17,8 +19,10 @@ function toImageBlock(image: ImageContent): ClaudeContentBlock {
function getLabelledParts(label: string, content: string | (TextContent | ImageContent)[]): ReplayPart[] {
if (typeof content === "string") return [`${label}\n${content}`];
const text = content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("\n");
const images = content.filter((block) => block.type === "image");
return [text ? `${label}\n${text}` : label, ...images];
}
@@ -26,26 +30,33 @@ function getReplayParts(message: Message): ReplayPart[] {
switch (message.role) {
case "system":
return [];
case "user":
return getLabelledParts("[user]", message.content);
case "assistant":
return message.content.flatMap((block) => {
if (block.type === "text") return [`[assistant]\n${block.text}`];
if (block.type === "toolCall") return [`[tool call ${block.name}] ${JSON.stringify(block.arguments)}`];
return [];
});
case "toolResult":
return getLabelledParts(message.isError ? `[tool result ${message.toolName} error]` : `[tool result ${message.toolName}]`, message.content);
case "toolResult": {
const label = message.isError ? `[tool result ${message.toolName} error]` : `[tool result ${message.toolName}]`;
return getLabelledParts(label, message.content);
}
}
}
function toContentBlocks(parts: ReplayPart[]): ClaudeContentBlock[] {
const blocks: ClaudeContentBlock[] = [];
let pendingText: string[] = [];
const flushText = () => {
if (pendingText.length > 0) blocks.push({ type: "text", text: pendingText.join("\n\n") });
pendingText = [];
};
for (const part of parts) {
if (typeof part === "string") {
pendingText.push(part);
@@ -54,20 +65,33 @@ function toContentBlocks(parts: ReplayPart[]): ClaudeContentBlock[] {
blocks.push(toImageBlock(part));
}
}
flushText();
return blocks;
}
function toUnlabelledBlocks(content: string | (TextContent | ImageContent)[]): ClaudeContentBlock[] {
if (typeof content === "string") return [{ type: "text", text: content }];
return content.map((block) => (block.type === "text" ? { type: "text", text: block.text } : toImageBlock(block)));
}
function toUserContent(conversation: Message[]): ClaudeContentBlock[] {
const [first] = conversation;
if (conversation.length === 1 && first?.role === "user") return toUnlabelledBlocks(first.content);
return toContentBlocks(conversation.flatMap(getReplayParts));
}
export function toClaudeInput(messages: Message[]): ClaudeInput {
const conversation = messages.filter((message) => message.role !== "system");
const onlyMessage = conversation.length === 1 ? conversation[0] : null;
const userContent = onlyMessage?.role === "user" ? toUnlabelledBlocks(onlyMessage.content) : toContentBlocks(conversation.flatMap(getReplayParts));
return { systemPrompt: getCurrentSystemPrompt(messages), tools: getCurrentTools(messages), userContent };
return {
systemPrompt: getCurrentSystemPrompt(messages),
tools: getCurrentTools(messages),
userContent: toUserContent(conversation),
};
}
export function toStreamJsonLine(content: ClaudeContentBlock[]): string {