merge: sync upstream pi-mono main into sproutclaw
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
Merge upstream/main (195 commits) while keeping sproutclaw-specific README, webui workflow docs, and restored upstream .pi prompt templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import { APP_NAME } from "../config.js";
|
||||
import { APP_NAME } from "../config.ts";
|
||||
|
||||
process.title = APP_NAME;
|
||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||
|
||||
import { restoreSandboxEnv } from "./restore-sandbox-env.js";
|
||||
import { restoreSandboxEnv } from "./restore-sandbox-env.ts";
|
||||
|
||||
restoreSandboxEnv();
|
||||
|
||||
await import("./register-bedrock.js");
|
||||
await import("../cli.js");
|
||||
await import("./register-bedrock.ts");
|
||||
await import("../cli.ts");
|
||||
|
||||
@@ -5,18 +5,16 @@
|
||||
*
|
||||
* Test with: npx tsx src/cli-new.ts [args...]
|
||||
*/
|
||||
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
||||
import { APP_NAME } from "./config.js";
|
||||
import { main } from "./main.js";
|
||||
import { APP_NAME } from "./config.ts";
|
||||
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
|
||||
import { main } from "./main.ts";
|
||||
|
||||
process.title = APP_NAME;
|
||||
process.env.PI_CODING_AGENT = "true";
|
||||
process.emitWarning = (() => {}) as typeof process.emitWarning;
|
||||
|
||||
// bodyTimeout/headersTimeout default to 300s in undici; long local-LLM stalls
|
||||
// (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream
|
||||
// with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own
|
||||
// AbortController-based deadlines via retry.provider.timeoutMs.
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));
|
||||
// Configure undici's global dispatcher before provider SDKs issue requests.
|
||||
// Runtime settings are applied once SettingsManager has loaded global/project settings.
|
||||
configureHttpDispatcher();
|
||||
|
||||
main(process.argv.slice(2));
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.js";
|
||||
import type { ExtensionFlag } from "../core/extensions/types.js";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR } from "../config.ts";
|
||||
import type { ExtensionFlag } from "../core/extensions/types.ts";
|
||||
|
||||
export type Mode = "text" | "json" | "rpc";
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
|
||||
import type { ResolvedPaths } from "../core/package-manager.js";
|
||||
import type { SettingsManager } from "../core/settings-manager.js";
|
||||
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
|
||||
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
||||
import type { ResolvedPaths } from "../core/package-manager.ts";
|
||||
import type { SettingsManager } from "../core/settings-manager.ts";
|
||||
import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.ts";
|
||||
import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.ts";
|
||||
|
||||
export interface ConfigSelectorOptions {
|
||||
resolvedPaths: ResolvedPaths;
|
||||
@@ -43,6 +43,7 @@ export async function selectConfig(options: ConfigSelectorOptions): Promise<void
|
||||
process.exit(0);
|
||||
},
|
||||
() => ui.requestRender(),
|
||||
ui.terminal.rows,
|
||||
);
|
||||
|
||||
ui.addChild(selector);
|
||||
|
||||
@@ -6,9 +6,9 @@ import { access, readFile, stat } from "node:fs/promises";
|
||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { resolve } from "path";
|
||||
import { resolveReadPath } from "../core/tools/path-utils.js";
|
||||
import { formatDimensionNote, resizeImage } from "../utils/image-resize.js";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.js";
|
||||
import { resolveReadPath } from "../core/tools/path-utils.ts";
|
||||
import { formatDimensionNote, resizeImage } from "../utils/image-resize.ts";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime.ts";
|
||||
|
||||
export interface ProcessedFiles {
|
||||
text: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ImageContent } from "@earendil-works/pi-ai";
|
||||
import type { Args } from "./args.js";
|
||||
import type { Args } from "./args.ts";
|
||||
|
||||
export interface InitialMessageInput {
|
||||
parsed: Args;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { formatNoModelsAvailableMessage } from "../core/auth-guidance.js";
|
||||
import type { ModelRegistry } from "../core/model-registry.js";
|
||||
import { formatNoModelsAvailableMessage } from "../core/auth-guidance.ts";
|
||||
import type { ModelRegistry } from "../core/model-registry.ts";
|
||||
|
||||
/**
|
||||
* Format a number as human-readable (e.g., 200000 -> "200K", 1000000 -> "1M")
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*/
|
||||
|
||||
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
|
||||
import { KeybindingsManager } from "../core/keybindings.js";
|
||||
import type { SessionInfo, SessionListProgress } from "../core/session-manager.js";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js";
|
||||
import { KeybindingsManager } from "../core/keybindings.ts";
|
||||
import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts";
|
||||
import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts";
|
||||
|
||||
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import { accessSync, constants, existsSync, readFileSync, realpathSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, join, resolve, sep, win32 } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { shouldUseWindowsShell } from "./utils/child-process.js";
|
||||
import { spawnProcessSync } from "./utils/child-process.ts";
|
||||
import { normalizePath } from "./utils/paths.ts";
|
||||
|
||||
// =============================================================================
|
||||
// Package Detection
|
||||
@@ -111,21 +111,21 @@ function getSelfUpdateCommandForMethod(
|
||||
return undefined;
|
||||
case "pnpm":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("pnpm", ["install", "-g", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("pnpm", ["install", "-g", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]),
|
||||
);
|
||||
case "yarn":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]),
|
||||
);
|
||||
case "bun":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("bun", ["install", "-g", updatePackageName]),
|
||||
makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
|
||||
@@ -134,7 +134,13 @@ function getSelfUpdateCommandForMethod(
|
||||
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
||||
const inferred = npmCommand?.length ? undefined : getInferredNpmInstall();
|
||||
const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])];
|
||||
const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updatePackageName]);
|
||||
const installStep = makeSelfUpdateCommandStep(command, [
|
||||
...prefixArgs,
|
||||
"install",
|
||||
"-g",
|
||||
"--ignore-scripts",
|
||||
updatePackageName,
|
||||
]);
|
||||
const uninstallStep =
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
@@ -151,10 +157,9 @@ function readCommandOutput(
|
||||
args: string[],
|
||||
options: { requireSuccess?: boolean } = {},
|
||||
): string | undefined {
|
||||
const result = spawnSync(command, args, {
|
||||
const result = spawnProcessSync(command, args, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: shouldUseWindowsShell(command),
|
||||
});
|
||||
if (result.status === 0) return result.stdout.trim() || undefined;
|
||||
if (options.requireSuccess) {
|
||||
@@ -207,16 +212,18 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined {
|
||||
const resolvedPath = resolve(path);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
return undefined;
|
||||
}
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
let normalizedPath = resolvedPath;
|
||||
if (resolveSymlinks) {
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
normalizedPath = normalizedPath.toLowerCase();
|
||||
@@ -224,6 +231,29 @@ function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
function getPathComparisonCandidates(path: string): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter(
|
||||
(candidate): candidate is string => !!candidate,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getEntrypointPackageDir(): string | undefined {
|
||||
const entrypoint = process.argv[1];
|
||||
if (!entrypoint) return undefined;
|
||||
let dir = dirname(entrypoint);
|
||||
while (dir !== dirname(dir)) {
|
||||
if (existsSync(join(dir, "package.json"))) {
|
||||
return dir;
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isSelfUpdatePathWritable(): boolean {
|
||||
const packageDir = getPackageDir();
|
||||
try {
|
||||
@@ -236,17 +266,14 @@ function isSelfUpdatePathWritable(): boolean {
|
||||
}
|
||||
|
||||
function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean {
|
||||
const packageDir = normalizeExistingPathForComparison(getPackageDir());
|
||||
return (
|
||||
!!packageDir &&
|
||||
getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||
const normalizedRoot = normalizeExistingPathForComparison(root);
|
||||
return (
|
||||
!!normalizedRoot &&
|
||||
packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`)
|
||||
);
|
||||
})
|
||||
);
|
||||
const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!dir);
|
||||
const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir));
|
||||
return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||
return getPathComparisonCandidates(root).some((normalizedRoot) => {
|
||||
const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
||||
return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getSelfUpdateCommand(
|
||||
@@ -304,9 +331,7 @@ export function getPackageDir(): string {
|
||||
// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
|
||||
const envDir = process.env.PI_PACKAGE_DIR;
|
||||
if (envDir) {
|
||||
if (envDir === "~") return homedir();
|
||||
if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
|
||||
return envDir;
|
||||
return normalizePath(envDir);
|
||||
}
|
||||
|
||||
if (isBunBinary) {
|
||||
@@ -428,9 +453,7 @@ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
||||
export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
|
||||
|
||||
export function expandTildePath(path: string): string {
|
||||
if (path === "~") return homedir();
|
||||
if (path.startsWith("~/")) return homedir() + path.slice(1);
|
||||
return path;
|
||||
return normalizePath(path);
|
||||
}
|
||||
|
||||
const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import type { AgentSession } from "./agent-session.js";
|
||||
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.js";
|
||||
import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { CreateAgentSessionResult } from "./sdk.js";
|
||||
import { assertSessionCwdExists } from "./session-cwd.js";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import type { AgentSession } from "./agent-session.ts";
|
||||
import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agent-session-services.ts";
|
||||
import type { ReplacedSessionContext, SessionShutdownEvent, SessionStartEvent } from "./extensions/index.ts";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.ts";
|
||||
import type { CreateAgentSessionResult } from "./sdk.ts";
|
||||
import { assertSessionCwdExists } from "./session-cwd.ts";
|
||||
import { SessionManager } from "./session-manager.ts";
|
||||
|
||||
/**
|
||||
* Result returned by runtime creation.
|
||||
@@ -67,14 +68,25 @@ function extractUserMessageText(content: string | Array<{ type: string; text?: s
|
||||
export class AgentSessionRuntime {
|
||||
private rebindSession?: (session: AgentSession) => Promise<void>;
|
||||
private beforeSessionInvalidate?: () => void;
|
||||
private _session: AgentSession;
|
||||
private _services: AgentSessionServices;
|
||||
private readonly createRuntime: CreateAgentSessionRuntimeFactory;
|
||||
private _diagnostics: AgentSessionRuntimeDiagnostic[];
|
||||
private _modelFallbackMessage?: string;
|
||||
|
||||
constructor(
|
||||
private _session: AgentSession,
|
||||
private _services: AgentSessionServices,
|
||||
private readonly createRuntime: CreateAgentSessionRuntimeFactory,
|
||||
private _diagnostics: AgentSessionRuntimeDiagnostic[] = [],
|
||||
private _modelFallbackMessage?: string,
|
||||
) {}
|
||||
_session: AgentSession,
|
||||
_services: AgentSessionServices,
|
||||
createRuntime: CreateAgentSessionRuntimeFactory,
|
||||
_diagnostics: AgentSessionRuntimeDiagnostic[] = [],
|
||||
_modelFallbackMessage?: string,
|
||||
) {
|
||||
this._session = _session;
|
||||
this._services = _services;
|
||||
this.createRuntime = createRuntime;
|
||||
this._diagnostics = _diagnostics;
|
||||
this._modelFallbackMessage = _modelFallbackMessage;
|
||||
}
|
||||
|
||||
get services(): AgentSessionServices {
|
||||
return this._services;
|
||||
@@ -281,12 +293,11 @@ export class AgentSessionRuntime {
|
||||
return { cancelled: false, selectedText };
|
||||
}
|
||||
|
||||
const sourceManager = SessionManager.open(currentSessionFile, sessionDir);
|
||||
const forkedSessionPath = sourceManager.createBranchedSession(targetLeafId);
|
||||
const sessionManager = SessionManager.open(currentSessionFile, sessionDir);
|
||||
const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId);
|
||||
if (!forkedSessionPath) {
|
||||
throw new Error("Failed to create forked session");
|
||||
}
|
||||
const sessionManager = SessionManager.open(forkedSessionPath, sessionDir);
|
||||
await this.teardownCurrent("fork", sessionManager.getSessionFile());
|
||||
this.apply(
|
||||
await this.createRuntime({
|
||||
@@ -327,7 +338,7 @@ export class AgentSessionRuntime {
|
||||
* @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided.
|
||||
*/
|
||||
async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
|
||||
const resolvedPath = resolve(inputPath);
|
||||
const resolvedPath = resolvePath(inputPath);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
throw new SessionImportFileNotFoundError(resolvedPath);
|
||||
}
|
||||
@@ -406,4 +417,4 @@ export {
|
||||
type CreateAgentSessionServicesOptions,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./agent-session-services.js";
|
||||
} from "./agent-session-services.ts";
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { join } from "node:path";
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AuthStorage } from "./auth-storage.js";
|
||||
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js";
|
||||
import { ModelRegistry } from "./model-registry.js";
|
||||
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js";
|
||||
import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
|
||||
import type { SessionManager } from "./session-manager.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { AuthStorage } from "./auth-storage.ts";
|
||||
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||
import { ModelRegistry } from "./model-registry.ts";
|
||||
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.ts";
|
||||
import { type CreateAgentSessionOptions, type CreateAgentSessionResult, createAgentSession } from "./sdk.ts";
|
||||
import type { SessionManager } from "./session-manager.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
|
||||
/**
|
||||
* Non-fatal issues collected while creating services or sessions.
|
||||
@@ -129,8 +130,8 @@ function applyExtensionFlagValues(
|
||||
export async function createAgentSessionServices(
|
||||
options: CreateAgentSessionServicesOptions,
|
||||
): Promise<AgentSessionServices> {
|
||||
const cwd = options.cwd;
|
||||
const agentDir = options.agentDir ?? getAgentDir();
|
||||
const cwd = resolvePath(options.cwd);
|
||||
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();
|
||||
const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json"));
|
||||
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
|
||||
const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json"));
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { basename, dirname } from "node:path";
|
||||
import type {
|
||||
Agent,
|
||||
AgentEvent,
|
||||
@@ -31,12 +31,14 @@ import {
|
||||
isContextOverflow,
|
||||
modelsAreEqual,
|
||||
resetApiProviders,
|
||||
streamSimple,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { theme } from "../modes/interactive/theme/theme.js";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.js";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.js";
|
||||
import { theme } from "../modes/interactive/theme/theme.ts";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { sleep } from "../utils/sleep.ts";
|
||||
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.ts";
|
||||
import { type BashResult, executeBashWithOperations } from "./bash-executor.ts";
|
||||
import {
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
@@ -46,10 +48,10 @@ import {
|
||||
generateBranchSummary,
|
||||
prepareCompaction,
|
||||
shouldCompact,
|
||||
} from "./compaction/index.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.js";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
||||
} from "./compaction/index.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import { exportSessionToHtml, type ToolHtmlRenderer } from "./export-html/index.ts";
|
||||
import { createToolHtmlRenderer } from "./export-html/tool-renderer.ts";
|
||||
import {
|
||||
type ContextUsage,
|
||||
type ExtensionCommandContextActions,
|
||||
@@ -74,21 +76,21 @@ import {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
wrapRegisteredTools,
|
||||
} from "./extensions/index.js";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { SlashCommandInfo } from "./slash-commands.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";
|
||||
import { createAllToolDefinitions } from "./tools/index.js";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
||||
} from "./extensions/index.ts";
|
||||
import { emitSessionShutdownEvent } from "./extensions/runner.ts";
|
||||
import type { BashExecutionMessage, CustomMessage } from "./messages.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.ts";
|
||||
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.ts";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.ts";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.ts";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
import type { SlashCommandInfo } from "./slash-commands.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.ts";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.ts";
|
||||
import { createAllToolDefinitions } from "./tools/index.ts";
|
||||
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Skill Block Parsing
|
||||
@@ -119,7 +121,12 @@ export function parseSkillBlock(text: string): ParsedSkillBlock | null {
|
||||
|
||||
/** Session-specific events that extend the core AgentEvent */
|
||||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| Exclude<AgentEvent, { type: "agent_end" }>
|
||||
| {
|
||||
type: "agent_end";
|
||||
messages: AgentMessage[];
|
||||
willRetry: boolean;
|
||||
}
|
||||
| {
|
||||
type: "queue_update";
|
||||
steering: readonly string[];
|
||||
@@ -179,6 +186,7 @@ export interface AgentSessionConfig {
|
||||
export interface ExtensionBindings {
|
||||
uiContext?: ExtensionUIContext;
|
||||
commandContextActions?: ExtensionCommandContextActions;
|
||||
abortHandler?: () => void;
|
||||
shutdownHandler?: ShutdownHandler;
|
||||
onError?: ExtensionErrorListener;
|
||||
}
|
||||
@@ -251,7 +259,6 @@ export class AgentSession {
|
||||
// Event subscription state
|
||||
private _unsubscribeAgent?: () => void;
|
||||
private _eventListeners: AgentSessionEventListener[] = [];
|
||||
private _agentEventQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
/** Tracks pending steering messages for UI display. Removed when delivered. */
|
||||
private _steeringMessages: string[] = [];
|
||||
@@ -271,8 +278,6 @@ export class AgentSession {
|
||||
// Retry state
|
||||
private _retryAbortController: AbortController | undefined = undefined;
|
||||
private _retryAttempt = 0;
|
||||
private _retryPromise: Promise<void> | undefined = undefined;
|
||||
private _retryResolve: (() => void) | undefined = undefined;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
@@ -293,6 +298,7 @@ export class AgentSession {
|
||||
private _sessionStartEvent: SessionStartEvent;
|
||||
private _extensionUIContext?: ExtensionUIContext;
|
||||
private _extensionCommandContextActions?: ExtensionCommandContextActions;
|
||||
private _extensionAbortHandler?: () => void;
|
||||
private _extensionShutdownHandler?: ShutdownHandler;
|
||||
private _extensionErrorListener?: ExtensionErrorListener;
|
||||
private _extensionErrorUnsubscriber?: () => void;
|
||||
@@ -367,6 +373,18 @@ export class AgentSession {
|
||||
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
||||
}
|
||||
|
||||
private async _getCompactionRequestAuth(model: Model<any>): Promise<{
|
||||
apiKey?: string;
|
||||
headers?: Record<string, string>;
|
||||
}> {
|
||||
if (this.agent.streamFn === streamSimple) {
|
||||
return this._getRequiredRequestAuth(model);
|
||||
}
|
||||
|
||||
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
||||
return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install tool hooks once on the Agent instance.
|
||||
*
|
||||
@@ -382,8 +400,6 @@ export class AgentSession {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this._agentEventQueue;
|
||||
|
||||
try {
|
||||
return await runner.emitToolCall({
|
||||
type: "tool_call",
|
||||
@@ -450,54 +466,7 @@ export class AgentSession {
|
||||
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
||||
|
||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||
private _handleAgentEvent = (event: AgentEvent): void => {
|
||||
// Create retry promise synchronously before queueing async processing.
|
||||
// Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()
|
||||
// as soon as agent.prompt() resolves. If _retryPromise is created only inside
|
||||
// _processAgentEvent, slow earlier queued events can delay agent_end processing
|
||||
// and waitForRetry() can miss the in-flight retry.
|
||||
this._createRetryPromiseForAgentEnd(event);
|
||||
|
||||
this._agentEventQueue = this._agentEventQueue.then(
|
||||
() => this._processAgentEvent(event),
|
||||
() => this._processAgentEvent(event),
|
||||
);
|
||||
|
||||
// Keep queue alive if an event handler fails
|
||||
this._agentEventQueue.catch(() => {});
|
||||
};
|
||||
|
||||
private _createRetryPromiseForAgentEnd(event: AgentEvent): void {
|
||||
if (event.type !== "agent_end" || this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastAssistant = this._findLastAssistantInMessages(event.messages);
|
||||
if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return message as AssistantMessage;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _processAgentEvent(event: AgentEvent): Promise<void> {
|
||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
||||
// This ensures the UI sees the updated queue state
|
||||
if (event.type === "message_start" && event.message.role === "user") {
|
||||
@@ -524,7 +493,7 @@ export class AgentSession {
|
||||
await this._emitExtensionEvent(event);
|
||||
|
||||
// Notify all listeners
|
||||
this._emit(event);
|
||||
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
||||
|
||||
// Handle session persistence
|
||||
if (event.type === "message_end") {
|
||||
@@ -568,30 +537,21 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check auto-retry and auto-compaction after agent completes
|
||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
private _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: "agent_end" }>): boolean {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for retryable errors first (overloaded, rate limit, server errors)
|
||||
if (this._isRetryableError(msg)) {
|
||||
const didRetry = await this._handleRetryableError(msg);
|
||||
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
||||
for (let i = event.messages.length - 1; i >= 0; i--) {
|
||||
const message = event.messages[i];
|
||||
if (message.role === "assistant") {
|
||||
return this._isRetryableError(message as AssistantMessage);
|
||||
}
|
||||
|
||||
this._resolveRetry();
|
||||
await this._checkCompaction(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the pending retry promise */
|
||||
private _resolveRetry(): void {
|
||||
if (this._retryResolve) {
|
||||
this._retryResolve();
|
||||
this._retryResolve = undefined;
|
||||
this._retryPromise = undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract text content from a message */
|
||||
@@ -617,7 +577,7 @@ export class AgentSession {
|
||||
|
||||
private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void {
|
||||
// Agent-core stores the finalized message object in its state before emitting message_end.
|
||||
// SessionManager persistence happens later in _processAgentEvent() with event.message.
|
||||
// SessionManager persistence happens later in _handleAgentEvent() with event.message.
|
||||
// Mutating this object in place keeps agent state, later turn/agent events, listeners,
|
||||
// and the eventual SessionManager.appendMessage(event.message) persistence in sync.
|
||||
if (target === replacement) {
|
||||
@@ -963,6 +923,41 @@ export class AgentSession {
|
||||
// Prompting
|
||||
// =========================================================================
|
||||
|
||||
private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {
|
||||
try {
|
||||
await this.agent.prompt(messages);
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlePostAgentRun(): Promise<boolean> {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = undefined;
|
||||
if (!msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.stopReason === "error" && this._retryAttempt > 0) {
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt,
|
||||
finalError: msg.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
}
|
||||
|
||||
return await this._checkCompaction(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt to the agent.
|
||||
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
||||
@@ -1053,8 +1048,15 @@ export class AgentSession {
|
||||
|
||||
// Check if we need to compact before sending (catches aborted responses)
|
||||
const lastAssistant = this._findLastAssistantMessage();
|
||||
if (lastAssistant) {
|
||||
await this._checkCompaction(lastAssistant, false);
|
||||
if (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {
|
||||
try {
|
||||
await this.agent.continue();
|
||||
while (await this._handlePostAgentRun()) {
|
||||
await this.agent.continue();
|
||||
}
|
||||
} finally {
|
||||
this._flushPendingBashMessages();
|
||||
}
|
||||
}
|
||||
|
||||
// Build messages array (custom message if any, then user message)
|
||||
@@ -1114,8 +1116,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
preflightResult?.(true);
|
||||
await this.agent.prompt(messages);
|
||||
await this.waitForRetry();
|
||||
await this._runAgentPrompt(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1301,7 +1302,7 @@ export class AgentSession {
|
||||
this.agent.steer(appMessage);
|
||||
}
|
||||
} else if (options?.triggerTurn) {
|
||||
await this.agent.prompt(appMessage);
|
||||
await this._runAgentPrompt(appMessage);
|
||||
} else {
|
||||
this.agent.state.messages.push(appMessage);
|
||||
this.sessionManager.appendCustomMessageEntry(
|
||||
@@ -1626,7 +1627,7 @@ export class AgentSession {
|
||||
throw new Error(formatNoModelSelectedMessage());
|
||||
}
|
||||
|
||||
const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
|
||||
const { apiKey, headers } = await this._getCompactionRequestAuth(this.model);
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
@@ -1684,6 +1685,7 @@ export class AgentSession {
|
||||
customInstructions,
|
||||
this._compactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = result.summary;
|
||||
firstKeptEntryId = result.firstKeptEntryId;
|
||||
@@ -1771,12 +1773,12 @@ export class AgentSession {
|
||||
* @param assistantMessage The assistant message to check
|
||||
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
||||
*/
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {
|
||||
private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
if (!settings.enabled) return;
|
||||
if (!settings.enabled) return false;
|
||||
|
||||
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
||||
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
||||
|
||||
const contextWindow = this.model?.contextWindow ?? 0;
|
||||
|
||||
@@ -1794,7 +1796,7 @@ export class AgentSession {
|
||||
const assistantIsFromBeforeCompaction =
|
||||
compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
|
||||
if (assistantIsFromBeforeCompaction) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 1: Overflow - LLM returned context overflow error
|
||||
@@ -1809,7 +1811,7 @@ export class AgentSession {
|
||||
errorMessage:
|
||||
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._overflowRecoveryAttempted = true;
|
||||
@@ -1819,8 +1821,7 @@ export class AgentSession {
|
||||
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
await this._runAutoCompaction("overflow", true);
|
||||
return;
|
||||
return await this._runAutoCompaction("overflow", true);
|
||||
}
|
||||
|
||||
// Case 2: Threshold - context is getting large
|
||||
@@ -1830,7 +1831,7 @@ export class AgentSession {
|
||||
if (assistantMessage.stopReason === "error") {
|
||||
const messages = this.agent.state.messages;
|
||||
const estimate = estimateContextTokens(messages);
|
||||
if (estimate.lastUsageIndex === null) return; // No usage data at all
|
||||
if (estimate.lastUsageIndex === null) return false; // No usage data at all
|
||||
// Verify the usage source is post-compaction. Kept pre-compaction messages
|
||||
// have stale usage reflecting the old (larger) context and would falsely
|
||||
// trigger compaction right after one just finished.
|
||||
@@ -1840,21 +1841,22 @@ export class AgentSession {
|
||||
usageMsg.role === "assistant" &&
|
||||
(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
contextTokens = estimate.tokens;
|
||||
} else {
|
||||
contextTokens = calculateContextTokens(assistantMessage.usage);
|
||||
}
|
||||
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
||||
await this._runAutoCompaction("threshold", false);
|
||||
return await this._runAutoCompaction("threshold", false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Run auto-compaction with events.
|
||||
*/
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<boolean> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
|
||||
this._emit({ type: "compaction_start", reason });
|
||||
@@ -1869,21 +1871,28 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||
if (!authResult.ok || !authResult.apiKey) {
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
let apiKey: string | undefined;
|
||||
let headers: Record<string, string> | undefined;
|
||||
if (this.agent.streamFn === streamSimple) {
|
||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||
if (!authResult.ok || !authResult.apiKey) {
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
apiKey = authResult.apiKey;
|
||||
headers = authResult.headers;
|
||||
} else {
|
||||
({ apiKey, headers } = await this._getCompactionRequestAuth(this.model));
|
||||
}
|
||||
const { apiKey, headers } = authResult;
|
||||
|
||||
const pathEntries = this.sessionManager.getBranch();
|
||||
|
||||
@@ -1896,7 +1905,7 @@ export class AgentSession {
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let extensionCompaction: CompactionResult | undefined;
|
||||
@@ -1919,7 +1928,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extensionResult?.compaction) {
|
||||
@@ -1949,6 +1958,7 @@ export class AgentSession {
|
||||
undefined,
|
||||
this._autoCompactionAbortController.signal,
|
||||
this.thinkingLevel,
|
||||
this.agent.streamFn,
|
||||
);
|
||||
summary = compactResult.summary;
|
||||
firstKeptEntryId = compactResult.firstKeptEntryId;
|
||||
@@ -1964,7 +1974,7 @@ export class AgentSession {
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
|
||||
@@ -1999,17 +2009,12 @@ export class AgentSession {
|
||||
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
||||
this.agent.state.messages = messages.slice(0, -1);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
} else if (this.agent.hasQueuedMessages()) {
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Kick the loop so queued messages are actually delivered.
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {});
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
||||
// Continue once so queued messages are delivered.
|
||||
return this.agent.hasQueuedMessages();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||
this._emit({
|
||||
@@ -2023,6 +2028,7 @@ export class AgentSession {
|
||||
? `Context overflow recovery failed: ${errorMessage}`
|
||||
: `Auto-compaction failed: ${errorMessage}`,
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this._autoCompactionAbortController = undefined;
|
||||
}
|
||||
@@ -2047,6 +2053,9 @@ export class AgentSession {
|
||||
if (bindings.commandContextActions !== undefined) {
|
||||
this._extensionCommandContextActions = bindings.commandContextActions;
|
||||
}
|
||||
if (bindings.abortHandler !== undefined) {
|
||||
this._extensionAbortHandler = bindings.abortHandler;
|
||||
}
|
||||
if (bindings.shutdownHandler !== undefined) {
|
||||
this._extensionShutdownHandler = bindings.shutdownHandler;
|
||||
}
|
||||
@@ -2211,7 +2220,13 @@ export class AgentSession {
|
||||
getModel: () => this.model,
|
||||
isIdle: () => !this.isStreaming,
|
||||
getSignal: () => this.agent.signal,
|
||||
abort: () => this.abort(),
|
||||
abort: () => {
|
||||
if (this._extensionAbortHandler) {
|
||||
this._extensionAbortHandler();
|
||||
return;
|
||||
}
|
||||
void this.abort();
|
||||
},
|
||||
hasPendingMessages: () => this.pendingMessageCount > 0,
|
||||
shutdown: () => {
|
||||
this._extensionShutdownHandler?.();
|
||||
@@ -2427,43 +2442,27 @@ export class AgentSession {
|
||||
if (isContextOverflow(message, contextWindow)) return false;
|
||||
|
||||
const err = message.errorMessage;
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, request ended without sending chunks, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
|
||||
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retryable errors with exponential backoff.
|
||||
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
||||
* Prepare a retryable error for continuation with exponential backoff.
|
||||
* @returns true if the caller should continue the agent, false otherwise
|
||||
*/
|
||||
private async _handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
||||
private async _prepareRetry(message: AssistantMessage): Promise<boolean> {
|
||||
const settings = this.settingsManager.getRetrySettings();
|
||||
if (!settings.enabled) {
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry promise is created synchronously in _handleAgentEvent for agent_end.
|
||||
// Keep a defensive fallback here in case a future refactor bypasses that path.
|
||||
if (!this._retryPromise) {
|
||||
this._retryPromise = new Promise((resolve) => {
|
||||
this._retryResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
this._retryAttempt++;
|
||||
|
||||
if (this._retryAttempt > settings.maxRetries) {
|
||||
// Max retries exceeded, emit final failure and reset
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt: this._retryAttempt - 1,
|
||||
finalError: message.errorMessage,
|
||||
});
|
||||
this._retryAttempt = 0;
|
||||
this._resolveRetry(); // Resolve so waitForRetry() completes
|
||||
// Preserve the completed attempt count so post-run handling can emit the final failure.
|
||||
this._retryAttempt--;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2491,24 +2490,16 @@ export class AgentSession {
|
||||
// Aborted during sleep - emit end event so UI can clean up
|
||||
const attempt = this._retryAttempt;
|
||||
this._retryAttempt = 0;
|
||||
this._retryAbortController = undefined;
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
attempt,
|
||||
finalError: "Retry cancelled",
|
||||
});
|
||||
this._resolveRetry();
|
||||
return false;
|
||||
} finally {
|
||||
this._retryAbortController = undefined;
|
||||
}
|
||||
this._retryAbortController = undefined;
|
||||
|
||||
// Retry via continue() - use setTimeout to break out of event handler chain
|
||||
setTimeout(() => {
|
||||
this.agent.continue().catch(() => {
|
||||
// Retry failed - will be caught by next agent_end
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2518,26 +2509,11 @@ export class AgentSession {
|
||||
*/
|
||||
abortRetry(): void {
|
||||
this._retryAbortController?.abort();
|
||||
// Note: _retryAttempt is reset in the catch block of _autoRetry
|
||||
this._resolveRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any in-progress retry to complete.
|
||||
* Returns immediately if no retry is in progress.
|
||||
*/
|
||||
private async waitForRetry(): Promise<void> {
|
||||
if (!this._retryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._retryPromise;
|
||||
await this.agent.waitForIdle();
|
||||
}
|
||||
|
||||
/** Whether auto-retry is currently in progress */
|
||||
get isRetrying(): boolean {
|
||||
return this._retryPromise !== undefined;
|
||||
return this._retryAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** Whether auto-retry is enabled */
|
||||
@@ -3026,7 +3002,10 @@ export class AgentSession {
|
||||
* @returns The resolved output file path.
|
||||
*/
|
||||
exportToJsonl(outputPath?: string): string {
|
||||
const filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
|
||||
const filePath = resolvePath(
|
||||
outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`,
|
||||
process.cwd(),
|
||||
);
|
||||
const dir = dirname(filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { getDocsPath } from "../config.js";
|
||||
import { getDocsPath } from "../config.ts";
|
||||
|
||||
const UNKNOWN_PROVIDER = "unknown";
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@earendil-w
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { resolveConfigValue } from "./resolve-config-value.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { normalizePath } from "../utils/paths.ts";
|
||||
import { resolveConfigValue } from "./resolve-config-value.ts";
|
||||
|
||||
export type ApiKeyCredential = {
|
||||
type: "api_key";
|
||||
@@ -50,7 +51,11 @@ export interface AuthStorageBackend {
|
||||
}
|
||||
|
||||
export class FileAuthStorageBackend implements AuthStorageBackend {
|
||||
constructor(private authPath: string = join(getAgentDir(), "auth.json")) {}
|
||||
private authPath: string;
|
||||
|
||||
constructor(authPath: string = join(getAgentDir(), "auth.json")) {
|
||||
this.authPath = normalizePath(authPath);
|
||||
}
|
||||
|
||||
private ensureParentDir(): void {
|
||||
const dir = dirname(this.authPath);
|
||||
@@ -194,8 +199,10 @@ export class AuthStorage {
|
||||
private fallbackResolver?: (provider: string) => string | undefined;
|
||||
private loadError: Error | null = null;
|
||||
private errors: Error[] = [];
|
||||
private storage: AuthStorageBackend;
|
||||
|
||||
private constructor(private storage: AuthStorageBackend) {
|
||||
private constructor(storage: AuthStorageBackend) {
|
||||
this.storage = storage;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
||||
import type { BashOperations } from "./tools/bash.js";
|
||||
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
||||
import { stripAnsi } from "../utils/ansi.ts";
|
||||
import { sanitizeBinaryOutput } from "../utils/shell.ts";
|
||||
import type { BashOperations } from "./tools/bash.ts";
|
||||
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "../messages.js";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js";
|
||||
import { estimateTokens } from "./compaction.js";
|
||||
} from "../messages.ts";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts";
|
||||
import { estimateTokens } from "./compaction.ts";
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
formatFileOperations,
|
||||
SUMMARIZATION_SYSTEM_PROMPT,
|
||||
serializeConversation,
|
||||
} from "./utils.js";
|
||||
} from "./utils.ts";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -44,7 +44,7 @@ export interface BranchSummaryDetails {
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
|
||||
export type { FileOperations } from "./utils.js";
|
||||
export type { FileOperations } from "./utils.ts";
|
||||
|
||||
export interface BranchPreparation {
|
||||
/** Messages extracted for summarization, in chronological order */
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
* and after compaction the session is reloaded.
|
||||
*/
|
||||
|
||||
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@earendil-works/pi-ai";
|
||||
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai";
|
||||
import { completeSimple } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
convertToLlm,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "../messages.js";
|
||||
import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.js";
|
||||
} from "../messages.ts";
|
||||
import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.ts";
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
formatFileOperations,
|
||||
SUMMARIZATION_SYSTEM_PROMPT,
|
||||
serializeConversation,
|
||||
} from "./utils.js";
|
||||
} from "./utils.ts";
|
||||
|
||||
// ============================================================================
|
||||
// File Operation Tracking
|
||||
@@ -523,6 +523,34 @@ Use this EXACT format:
|
||||
|
||||
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
||||
|
||||
function createSummarizationOptions(
|
||||
model: Model<any>,
|
||||
maxTokens: number,
|
||||
apiKey: string | undefined,
|
||||
headers: Record<string, string> | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
thinkingLevel: ThinkingLevel | undefined,
|
||||
): SimpleStreamOptions {
|
||||
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers };
|
||||
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
||||
options.reasoning = thinkingLevel;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function completeSummarization(
|
||||
model: Model<any>,
|
||||
context: Context,
|
||||
options: SimpleStreamOptions,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<AssistantMessage> {
|
||||
if (!streamFn) {
|
||||
return completeSimple(model, context, options);
|
||||
}
|
||||
const stream = await streamFn(model, context, options);
|
||||
return stream.result();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a summary of the conversation using the LLM.
|
||||
* If previousSummary is provided, uses the update prompt to merge.
|
||||
@@ -531,14 +559,18 @@ export async function generateSummary(
|
||||
currentMessages: AgentMessage[],
|
||||
model: Model<any>,
|
||||
reserveTokens: number,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
customInstructions?: string,
|
||||
previousSummary?: string,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.floor(0.8 * reserveTokens);
|
||||
const maxTokens = Math.min(
|
||||
Math.floor(0.8 * reserveTokens),
|
||||
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||
);
|
||||
|
||||
// Use update prompt if we have a previous summary, otherwise initial prompt
|
||||
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
||||
@@ -566,15 +598,13 @@ export async function generateSummary(
|
||||
},
|
||||
];
|
||||
|
||||
const completionOptions =
|
||||
model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
||||
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
|
||||
: { maxTokens, signal, apiKey, headers };
|
||||
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel);
|
||||
|
||||
const response = await completeSimple(
|
||||
const response = await completeSummarization(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
completionOptions,
|
||||
streamFn,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -717,11 +747,12 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
|
||||
export async function compact(
|
||||
preparation: CompactionPreparation,
|
||||
model: Model<any>,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
customInstructions?: string,
|
||||
signal?: AbortSignal,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<CompactionResult> {
|
||||
const {
|
||||
firstKeptEntryId,
|
||||
@@ -751,6 +782,7 @@ export async function compact(
|
||||
customInstructions,
|
||||
previousSummary,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
)
|
||||
: Promise.resolve("No prior history."),
|
||||
generateTurnPrefixSummary(
|
||||
@@ -761,6 +793,7 @@ export async function compact(
|
||||
headers,
|
||||
signal,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
),
|
||||
]);
|
||||
// Merge into single summary
|
||||
@@ -777,6 +810,7 @@ export async function compact(
|
||||
customInstructions,
|
||||
previousSummary,
|
||||
thinkingLevel,
|
||||
streamFn,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -803,12 +837,16 @@ async function generateTurnPrefixSummary(
|
||||
messages: AgentMessage[],
|
||||
model: Model<any>,
|
||||
reserveTokens: number,
|
||||
apiKey: string,
|
||||
apiKey: string | undefined,
|
||||
headers?: Record<string, string>,
|
||||
signal?: AbortSignal,
|
||||
thinkingLevel?: ThinkingLevel,
|
||||
streamFn?: StreamFn,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
||||
const maxTokens = Math.min(
|
||||
Math.floor(0.5 * reserveTokens),
|
||||
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
||||
); // Smaller budget for turn prefix
|
||||
const llmMessages = convertToLlm(messages);
|
||||
const conversationText = serializeConversation(llmMessages);
|
||||
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
||||
@@ -820,12 +858,11 @@ async function generateTurnPrefixSummary(
|
||||
},
|
||||
];
|
||||
|
||||
const response = await completeSimple(
|
||||
const response = await completeSummarization(
|
||||
model,
|
||||
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
||||
model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
||||
? { maxTokens, signal, apiKey, headers, reasoning: thinkingLevel }
|
||||
: { maxTokens, signal, apiKey, headers },
|
||||
createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel),
|
||||
streamFn,
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
* Compaction and summarization utilities.
|
||||
*/
|
||||
|
||||
export * from "./branch-summarization.js";
|
||||
export * from "./compaction.js";
|
||||
export * from "./utils.js";
|
||||
export * from "./branch-summarization.ts";
|
||||
export * from "./compaction.ts";
|
||||
export * from "./utils.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { waitForChildProcess } from "../utils/child-process.js";
|
||||
import { waitForChildProcess } from "../utils/child-process.ts";
|
||||
|
||||
/**
|
||||
* Options for executing shell commands.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgentState } from "@earendil-works/pi-agent-core";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { basename, join } from "path";
|
||||
import { APP_NAME, getExportTemplateDir } from "../../config.js";
|
||||
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import type { SessionEntry } from "../session-manager.js";
|
||||
import { SessionManager } from "../session-manager.js";
|
||||
import { APP_NAME, getExportTemplateDir } from "../../config.ts";
|
||||
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.ts";
|
||||
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import type { SessionEntry } from "../session-manager.ts";
|
||||
import { SessionManager } from "../session-manager.ts";
|
||||
|
||||
/**
|
||||
* Interface for rendering custom tools to HTML.
|
||||
@@ -270,7 +271,7 @@ export async function exportSessionToHtml(
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
||||
if (!outputPath) {
|
||||
const sessionBasename = basename(sessionFile, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
|
||||
@@ -286,12 +287,13 @@ export async function exportSessionToHtml(
|
||||
*/
|
||||
export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {
|
||||
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
||||
const resolvedInputPath = resolvePath(inputPath);
|
||||
|
||||
if (!existsSync(inputPath)) {
|
||||
throw new Error(`File not found: ${inputPath}`);
|
||||
if (!existsSync(resolvedInputPath)) {
|
||||
throw new Error(`File not found: ${resolvedInputPath}`);
|
||||
}
|
||||
|
||||
const sm = SessionManager.open(inputPath);
|
||||
const sm = SessionManager.open(resolvedInputPath);
|
||||
|
||||
const sessionData: SessionData = {
|
||||
header: sm.getHeader(),
|
||||
@@ -303,9 +305,9 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
||||
|
||||
const html = generateHtml(sessionData, opts.themeName);
|
||||
|
||||
let outputPath = opts.outputPath;
|
||||
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
||||
if (!outputPath) {
|
||||
const inputBasename = basename(inputPath, ".jsonl");
|
||||
const inputBasename = basename(resolvedInputPath, ".jsonl");
|
||||
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
|
||||
}
|
||||
|
||||
|
||||
@@ -910,7 +910,8 @@
|
||||
'</div>';
|
||||
};
|
||||
|
||||
let html = `<div class="tool-execution ${statusClass}">`;
|
||||
const toolDomId = `tool-call-${escapeHtml(call.id)}`;
|
||||
let html = `<div class="tool-execution ${statusClass}" id="${toolDomId}">`;
|
||||
const args = call.arguments || {};
|
||||
const name = call.name;
|
||||
|
||||
@@ -1445,6 +1446,16 @@
|
||||
// Cache for rendered entry DOM nodes
|
||||
const entryCache = new Map();
|
||||
|
||||
function getScrollTargetElementId(entryId) {
|
||||
const entry = byId.get(entryId);
|
||||
if (entry?.type === 'message' && entry.message.role === 'toolResult' && entry.message.toolCallId) {
|
||||
// getElementById() matches the parsed DOM id attribute, whose HTML entities
|
||||
// were already resolved from the escaped id rendered by renderToolCall().
|
||||
return `tool-call-${entry.message.toolCallId}`;
|
||||
}
|
||||
return `entry-${entryId}`;
|
||||
}
|
||||
|
||||
function renderEntryToNode(entry) {
|
||||
// Check cache first
|
||||
if (entryCache.has(entry.id)) {
|
||||
@@ -1506,9 +1517,12 @@
|
||||
if (scrollMode === 'bottom') {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
} else if (scrollMode === 'target') {
|
||||
// If scrollToEntryId is provided, scroll to that specific entry
|
||||
// If scrollToEntryId is provided, scroll to that specific entry.
|
||||
// Tool result entries are rendered inside their assistant tool-call block,
|
||||
// so route them to the visible tool-call element instead.
|
||||
const scrollTargetId = scrollToEntryId || targetId;
|
||||
const targetEl = document.getElementById(`entry-${scrollTargetId}`);
|
||||
const targetEl = document.getElementById(getScrollTargetElementId(scrollTargetId)) ||
|
||||
document.getElementById(`entry-${scrollTargetId}`);
|
||||
if (targetEl) {
|
||||
targetEl.scrollIntoView({ block: 'center' });
|
||||
// Briefly highlight the target message
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.js";
|
||||
import { ansiLinesToHtml } from "./ansi-to-html.js";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.ts";
|
||||
import { ansiLinesToHtml } from "./ansi-to-html.ts";
|
||||
|
||||
export interface ToolHtmlRendererDeps {
|
||||
/** Function to look up tool definition by name */
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* Extension system for lifecycle events and custom tools.
|
||||
*/
|
||||
|
||||
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js";
|
||||
export type { SourceInfo } from "../source-info.js";
|
||||
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.ts";
|
||||
export type { SourceInfo } from "../source-info.ts";
|
||||
export {
|
||||
createExtensionRuntime,
|
||||
discoverAndLoadExtensions,
|
||||
loadExtensionFromFactory,
|
||||
loadExtensions,
|
||||
} from "./loader.js";
|
||||
} from "./loader.ts";
|
||||
export type {
|
||||
ExtensionErrorListener,
|
||||
ForkHandler,
|
||||
@@ -17,8 +17,8 @@ export type {
|
||||
NewSessionHandler,
|
||||
ShutdownHandler,
|
||||
SwitchSessionHandler,
|
||||
} from "./runner.js";
|
||||
export { ExtensionRunner } from "./runner.js";
|
||||
} from "./runner.ts";
|
||||
export { ExtensionRunner } from "./runner.ts";
|
||||
export type {
|
||||
AfterProviderResponseEvent,
|
||||
AgentEndEvent,
|
||||
@@ -156,7 +156,7 @@ export type {
|
||||
WorkingIndicatorOptions,
|
||||
WriteToolCallEvent,
|
||||
WriteToolResultEvent,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
// Type guards
|
||||
export {
|
||||
defineTool,
|
||||
@@ -168,5 +168,5 @@ export {
|
||||
isReadToolResult,
|
||||
isToolCallEventType,
|
||||
isWriteToolResult,
|
||||
} from "./types.js";
|
||||
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js";
|
||||
} from "./types.ts";
|
||||
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
|
||||
@@ -20,14 +19,15 @@ import { createJiti } from "jiti";
|
||||
import * as _bundledTypebox from "typebox";
|
||||
import * as _bundledTypeboxCompile from "typebox/compile";
|
||||
import * as _bundledTypeboxValue from "typebox/value";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.ts";
|
||||
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
|
||||
// avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
|
||||
import * as _bundledPiCodingAgent from "../../index.js";
|
||||
import { createEventBus, type EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions } from "../exec.js";
|
||||
import { execCommand } from "../exec.js";
|
||||
import { createSyntheticSourceInfo } from "../source-info.js";
|
||||
import * as _bundledPiCodingAgent from "../../index.ts";
|
||||
import { resolvePath } from "../../utils/paths.ts";
|
||||
import { createEventBus, type EventBus } from "../event-bus.ts";
|
||||
import type { ExecOptions } from "../exec.ts";
|
||||
import { execCommand } from "../exec.ts";
|
||||
import { createSyntheticSourceInfo } from "../source-info.ts";
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionAPI,
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
ProviderConfig,
|
||||
RegisteredCommand,
|
||||
ToolDefinition,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
|
||||
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
|
||||
const VIRTUAL_MODULES: Record<string, unknown> = {
|
||||
@@ -115,31 +115,6 @@ function getAliases(): Record<string, string> {
|
||||
return _aliases;
|
||||
}
|
||||
|
||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||
|
||||
function normalizeUnicodeSpaces(str: string): string {
|
||||
return str.replace(UNICODE_SPACES, " ");
|
||||
}
|
||||
|
||||
function expandPath(p: string): string {
|
||||
const normalized = normalizeUnicodeSpaces(p);
|
||||
if (normalized.startsWith("~/")) {
|
||||
return path.join(os.homedir(), normalized.slice(2));
|
||||
}
|
||||
if (normalized.startsWith("~")) {
|
||||
return path.join(os.homedir(), normalized.slice(1));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolvePath(extPath: string, cwd: string): string {
|
||||
const expanded = expandPath(extPath);
|
||||
if (path.isAbsolute(expanded)) {
|
||||
return expanded;
|
||||
}
|
||||
return path.resolve(cwd, expanded);
|
||||
}
|
||||
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
@@ -236,7 +211,7 @@ function createExtensionAPI(
|
||||
shortcut: KeyId,
|
||||
options: {
|
||||
description?: string;
|
||||
handler: (ctx: import("./types.js").ExtensionContext) => Promise<void> | void;
|
||||
handler: (ctx: import("./types.ts").ExtensionContext) => Promise<void> | void;
|
||||
},
|
||||
): void {
|
||||
runtime.assertActive();
|
||||
@@ -396,7 +371,7 @@ async function loadExtension(
|
||||
eventBus: EventBus,
|
||||
runtime: ExtensionRuntime,
|
||||
): Promise<{ extension: Extension | null; error: string | null }> {
|
||||
const resolvedPath = resolvePath(extensionPath, cwd);
|
||||
const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });
|
||||
|
||||
try {
|
||||
const factory = await loadExtensionModule(resolvedPath);
|
||||
@@ -426,7 +401,8 @@ export async function loadExtensionFromFactory(
|
||||
extensionPath = "<inline>",
|
||||
): Promise<Extension> {
|
||||
const extension = createExtension(extensionPath, extensionPath);
|
||||
const api = createExtensionAPI(extension, runtime, cwd, eventBus);
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);
|
||||
await factory(api);
|
||||
return extension;
|
||||
}
|
||||
@@ -437,11 +413,12 @@ export async function loadExtensionFromFactory(
|
||||
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
|
||||
const extensions: Extension[] = [];
|
||||
const errors: Array<{ path: string; error: string }> = [];
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedEventBus = eventBus ?? createEventBus();
|
||||
const runtime = createExtensionRuntime();
|
||||
|
||||
for (const extPath of paths) {
|
||||
const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
|
||||
const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);
|
||||
|
||||
if (error) {
|
||||
errors.push({ path: extPath, error });
|
||||
@@ -578,6 +555,8 @@ export async function discoverAndLoadExtensions(
|
||||
agentDir: string = getAgentDir(),
|
||||
eventBus?: EventBus,
|
||||
): Promise<LoadExtensionsResult> {
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
const allPaths: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -592,16 +571,16 @@ export async function discoverAndLoadExtensions(
|
||||
};
|
||||
|
||||
// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/
|
||||
const localExtDir = path.join(cwd, CONFIG_DIR_NAME, "extensions");
|
||||
const localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, "extensions");
|
||||
addPaths(discoverExtensionsInDir(localExtDir));
|
||||
|
||||
// 2. Global extensions: agentDir/extensions/
|
||||
const globalExtDir = path.join(agentDir, "extensions");
|
||||
const globalExtDir = path.join(resolvedAgentDir, "extensions");
|
||||
addPaths(discoverExtensionsInDir(globalExtDir));
|
||||
|
||||
// 3. Explicitly configured paths
|
||||
for (const p of configuredPaths) {
|
||||
const resolved = resolvePath(p, cwd);
|
||||
const resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });
|
||||
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
||||
// Check for package.json with pi manifest or index.ts
|
||||
const entries = resolveExtensionEntries(resolved);
|
||||
@@ -617,5 +596,5 @@ export async function discoverAndLoadExtensions(
|
||||
addPaths([resolved]);
|
||||
}
|
||||
|
||||
return loadExtensions(allPaths, cwd, eventBus);
|
||||
return loadExtensions(allPaths, resolvedCwd, eventBus);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import type { ImageContent, Model } from "@earendil-works/pi-ai";
|
||||
import type { KeyId } from "@earendil-works/pi-tui";
|
||||
import { type Theme, theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ResourceDiagnostic } from "../diagnostics.js";
|
||||
import type { KeybindingsConfig } from "../keybindings.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
import { type Theme, theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ResourceDiagnostic } from "../diagnostics.ts";
|
||||
import type { KeybindingsConfig } from "../keybindings.ts";
|
||||
import type { ModelRegistry } from "../model-registry.ts";
|
||||
import type { SessionManager } from "../session-manager.ts";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
import type {
|
||||
BeforeAgentStartEvent,
|
||||
BeforeAgentStartEventResult,
|
||||
@@ -55,7 +55,7 @@ import type {
|
||||
ToolResultEventResult,
|
||||
UserBashEvent,
|
||||
UserBashEventResult,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
|
||||
// Extension shortcuts compete with canonical keybinding ids from keybindings.json.
|
||||
// Only editor-global shortcuts are reserved here. Picker-specific bindings are not.
|
||||
|
||||
@@ -40,27 +40,27 @@ import type {
|
||||
TUI,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { Static, TSchema } from "typebox";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { BashResult } from "../bash-executor.js";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
|
||||
import type { EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js";
|
||||
import type { KeybindingsManager } from "../keybindings.js";
|
||||
import type { CustomMessage } from "../messages.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { BashResult } from "../bash-executor.ts";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
|
||||
import type { EventBus } from "../event-bus.ts";
|
||||
import type { ExecOptions, ExecResult } from "../exec.ts";
|
||||
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
|
||||
import type { KeybindingsManager } from "../keybindings.ts";
|
||||
import type { CustomMessage } from "../messages.ts";
|
||||
import type { ModelRegistry } from "../model-registry.ts";
|
||||
import type {
|
||||
BranchSummaryEntry,
|
||||
CompactionEntry,
|
||||
ReadonlySessionManager,
|
||||
SessionEntry,
|
||||
SessionManager,
|
||||
} from "../session-manager.js";
|
||||
import type { SlashCommandInfo } from "../slash-commands.js";
|
||||
import type { SourceInfo } from "../source-info.js";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
import type { BashOperations } from "../tools/bash.js";
|
||||
import type { EditToolDetails } from "../tools/edit.js";
|
||||
} from "../session-manager.ts";
|
||||
import type { SlashCommandInfo } from "../slash-commands.ts";
|
||||
import type { SourceInfo } from "../source-info.ts";
|
||||
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
import type { BashOperations } from "../tools/bash.ts";
|
||||
import type { EditToolDetails } from "../tools/edit.ts";
|
||||
import type {
|
||||
BashToolDetails,
|
||||
BashToolInput,
|
||||
@@ -74,12 +74,12 @@ import type {
|
||||
ReadToolDetails,
|
||||
ReadToolInput,
|
||||
WriteToolInput,
|
||||
} from "../tools/index.js";
|
||||
} from "../tools/index.ts";
|
||||
|
||||
export type { ExecOptions, ExecResult } from "../exec.js";
|
||||
export type { BuildSystemPromptOptions } from "../system-prompt.js";
|
||||
export type { ExecOptions, ExecResult } from "../exec.ts";
|
||||
export type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
||||
export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
|
||||
export type { AppKeybinding, KeybindingsManager } from "../keybindings.js";
|
||||
export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
|
||||
|
||||
// ============================================================================
|
||||
// UI Context
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
*/
|
||||
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.js";
|
||||
import type { ExtensionRunner } from "./runner.js";
|
||||
import type { RegisteredTool } from "./types.js";
|
||||
import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.ts";
|
||||
import type { ExtensionRunner } from "./runner.ts";
|
||||
import type { RegisteredTool } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Wrap a RegisteredTool into an AgentTool.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type ExecFileException, execFile, spawnSync } from "child_process";
|
||||
import { existsSync, type FSWatcher, readFileSync, statSync, unwatchFile, watchFile } from "fs";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js";
|
||||
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.ts";
|
||||
|
||||
type GitPaths = {
|
||||
repoDir: string;
|
||||
|
||||
55
packages/coding-agent/src/core/http-dispatcher.ts
Normal file
55
packages/coding-agent/src/core/http-dispatcher.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as undici from "undici";
|
||||
|
||||
export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
|
||||
|
||||
export const HTTP_IDLE_TIMEOUT_CHOICES = [
|
||||
{ label: "30 sec", timeoutMs: 30_000 },
|
||||
{ label: "1 min", timeoutMs: 60_000 },
|
||||
{ label: "2 min", timeoutMs: 120_000 },
|
||||
{ label: "5 min", timeoutMs: 300_000 },
|
||||
{ label: "disabled", timeoutMs: 0 },
|
||||
] as const;
|
||||
|
||||
export function parseHttpIdleTimeoutMs(value: unknown): number | undefined {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.toLowerCase() === "disabled") {
|
||||
return 0;
|
||||
}
|
||||
if (trimmed.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return parseHttpIdleTimeoutMs(Number(trimmed));
|
||||
}
|
||||
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
export function formatHttpIdleTimeoutMs(timeoutMs: number): string {
|
||||
const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);
|
||||
if (choice) {
|
||||
return choice.label;
|
||||
}
|
||||
return `${timeoutMs / 1000} sec`;
|
||||
}
|
||||
|
||||
export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {
|
||||
const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
|
||||
if (normalizedTimeoutMs === undefined) {
|
||||
throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
|
||||
}
|
||||
undici.setGlobalDispatcher(
|
||||
new undici.EnvHttpProxyAgent({
|
||||
allowH2: false,
|
||||
bodyTimeout: normalizedTimeoutMs,
|
||||
headersTimeout: normalizedTimeoutMs,
|
||||
}),
|
||||
);
|
||||
// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's
|
||||
// bundled fetch can otherwise consume compressed responses through npm undici's
|
||||
// dispatcher without decompressing them, causing response.json() failures.
|
||||
undici.install?.();
|
||||
}
|
||||
@@ -10,13 +10,13 @@ export {
|
||||
type ModelCycleResult,
|
||||
type PromptOptions,
|
||||
type SessionStats,
|
||||
} from "./agent-session.js";
|
||||
} from "./agent-session.ts";
|
||||
export {
|
||||
AgentSessionRuntime,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
type CreateAgentSessionRuntimeResult,
|
||||
createAgentSessionRuntime,
|
||||
} from "./agent-session-runtime.js";
|
||||
} from "./agent-session-runtime.ts";
|
||||
export {
|
||||
type AgentSessionRuntimeDiagnostic,
|
||||
type AgentSessionServices,
|
||||
@@ -24,10 +24,10 @@ export {
|
||||
type CreateAgentSessionServicesOptions,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./agent-session-services.js";
|
||||
export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.js";
|
||||
export type { CompactionResult } from "./compaction/index.js";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js";
|
||||
} from "./agent-session-services.ts";
|
||||
export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.ts";
|
||||
export type { CompactionResult } from "./compaction/index.ts";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./event-bus.ts";
|
||||
// Extensions system
|
||||
export {
|
||||
type AgentEndEvent,
|
||||
@@ -73,5 +73,5 @@ export {
|
||||
type TurnEndEvent,
|
||||
type TurnStartEvent,
|
||||
type WorkingIndicatorOptions,
|
||||
} from "./extensions/index.js";
|
||||
export { createSyntheticSourceInfo } from "./source-info.js";
|
||||
} from "./extensions/index.ts";
|
||||
export { createSyntheticSourceInfo } from "./source-info.ts";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
|
||||
export interface AppKeybindings {
|
||||
"app.interrupt": true;
|
||||
|
||||
@@ -24,15 +24,16 @@ import { join } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { Compile } from "typebox/compile";
|
||||
import type { TLocalizedValidationError } from "typebox/error";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import type { AuthStatus, AuthStorage } from "./auth-storage.js";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { normalizePath } from "../utils/paths.ts";
|
||||
import type { AuthStatus, AuthStorage } from "./auth-storage.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.ts";
|
||||
import {
|
||||
clearConfigValueCache,
|
||||
resolveConfigValueOrThrow,
|
||||
resolveConfigValueUncached,
|
||||
resolveHeadersOrThrow,
|
||||
} from "./resolve-config-value.js";
|
||||
} from "./resolve-config-value.ts";
|
||||
|
||||
// Schema for OpenRouter routing preferences
|
||||
const PercentileCutoffsSchema = Type.Object({
|
||||
@@ -334,11 +335,12 @@ export class ModelRegistry {
|
||||
private modelRequestHeaders: Map<string, Record<string, string>> = new Map();
|
||||
private registeredProviders: Map<string, ProviderConfigInput> = new Map();
|
||||
private loadError: string | undefined = undefined;
|
||||
readonly authStorage: AuthStorage;
|
||||
private modelsJsonPath: string | undefined;
|
||||
|
||||
private constructor(
|
||||
readonly authStorage: AuthStorage,
|
||||
private modelsJsonPath: string | undefined,
|
||||
) {
|
||||
private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) {
|
||||
this.authStorage = authStorage;
|
||||
this.modelsJsonPath = modelsJsonPath ? normalizePath(modelsJsonPath) : undefined;
|
||||
this.loadModels();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@earendil-works/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { minimatch } from "minimatch";
|
||||
import { isValidThinkingLevel } from "../cli/args.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import type { ModelRegistry } from "./model-registry.js";
|
||||
import { isValidThinkingLevel } from "../cli/args.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import type { ModelRegistry } from "./model-registry.ts";
|
||||
|
||||
/** Default model IDs for each known provider */
|
||||
export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ChildProcess, type ChildProcessByStdio, spawn, spawnSync } from "node:child_process";
|
||||
import type { ChildProcess, ChildProcessByStdio } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
@@ -27,12 +27,12 @@ import type { Readable } from "node:stream";
|
||||
import { globSync } from "glob";
|
||||
import ignore from "ignore";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { shouldUseWindowsShell } from "../utils/child-process.js";
|
||||
import { type GitSource, parseGitUrl } from "../utils/git.js";
|
||||
import { canonicalizePath, isLocalPath } from "../utils/paths.js";
|
||||
import { isStdoutTakenOver } from "./output-guard.js";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts";
|
||||
import { type GitSource, parseGitUrl } from "../utils/git.ts";
|
||||
import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync, resolvePath } from "../utils/paths.ts";
|
||||
import { isStdoutTakenOver } from "./output-guard.ts";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.ts";
|
||||
|
||||
const NETWORK_TIMEOUT_MS = 10000;
|
||||
const UPDATE_CHECK_CONCURRENCY = 4;
|
||||
@@ -763,8 +763,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||
private progressCallback: ProgressCallback | undefined;
|
||||
|
||||
constructor(options: PackageManagerOptions) {
|
||||
this.cwd = options.cwd;
|
||||
this.agentDir = options.agentDir;
|
||||
this.cwd = resolvePath(options.cwd);
|
||||
this.agentDir = resolvePath(options.agentDir);
|
||||
this.settingsManager = options.settingsManager;
|
||||
}
|
||||
|
||||
@@ -778,9 +778,21 @@ export class DefaultPackageManager implements PackageManager {
|
||||
scope === "project" ? this.settingsManager.getProjectSettings() : this.settingsManager.getGlobalSettings();
|
||||
const currentPackages = currentSettings.packages ?? [];
|
||||
const normalizedSource = this.normalizePackageSourceForSettings(source, scope);
|
||||
const exists = currentPackages.some((existing) => this.packageSourcesMatch(existing, source, scope));
|
||||
if (exists) {
|
||||
return false;
|
||||
const matchIndex = currentPackages.findIndex((existing) => this.packageSourcesMatch(existing, source, scope));
|
||||
if (matchIndex !== -1) {
|
||||
const existing = currentPackages[matchIndex];
|
||||
if (this.getPackageSourceString(existing) === normalizedSource) {
|
||||
return false;
|
||||
}
|
||||
const nextPackages = [...currentPackages];
|
||||
nextPackages[matchIndex] =
|
||||
typeof existing === "string" ? normalizedSource : { ...existing, source: normalizedSource };
|
||||
if (scope === "project") {
|
||||
this.settingsManager.setProjectPackages(nextPackages);
|
||||
} else {
|
||||
this.settingsManager.setPackages(nextPackages);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const nextPackages = [...currentPackages, normalizedSource];
|
||||
if (scope === "project") {
|
||||
@@ -1084,7 +1096,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise<boolean> {
|
||||
const installedPath = this.getNpmInstallPath(source, scope);
|
||||
const installedPath = this.getManagedNpmInstallPath(source, scope);
|
||||
const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
|
||||
if (!installedVersion) {
|
||||
return true;
|
||||
@@ -1114,13 +1126,9 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise<void> {
|
||||
if (scope === "user") {
|
||||
await this.runNpmCommand(["install", "-g", ...specs]);
|
||||
return;
|
||||
}
|
||||
const installRoot = this.getNpmInstallRoot(scope, false);
|
||||
this.ensureNpmProject(installRoot);
|
||||
await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]);
|
||||
await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot));
|
||||
}
|
||||
|
||||
async checkForAvailableUpdates(): Promise<PackageUpdate[]> {
|
||||
@@ -1221,13 +1229,14 @@ export class DefaultPackageManager implements PackageManager {
|
||||
};
|
||||
|
||||
if (parsed.type === "npm") {
|
||||
const installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
let installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
const needsInstall =
|
||||
!existsSync(installedPath) ||
|
||||
(parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath)));
|
||||
if (needsInstall) {
|
||||
const installed = await installMissing();
|
||||
if (!installed) continue;
|
||||
installedPath = this.getNpmInstallPath(parsed, scope);
|
||||
}
|
||||
metadata.baseDir = installedPath;
|
||||
this.collectPackageResources(installedPath, accumulator, filter, metadata);
|
||||
@@ -1668,6 +1677,14 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return { command, args };
|
||||
}
|
||||
|
||||
private getPackageManagerName(): string {
|
||||
const npmCommand = this.getNpmCommand();
|
||||
const commandParts = [npmCommand.command, ...npmCommand.args];
|
||||
const separatorIndex = commandParts.lastIndexOf("--");
|
||||
const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command;
|
||||
return packageManagerCommand ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") : "";
|
||||
}
|
||||
|
||||
private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise<void> {
|
||||
const npmCommand = this.getNpmCommand();
|
||||
await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options);
|
||||
@@ -1686,31 +1703,44 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]);
|
||||
}
|
||||
|
||||
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {
|
||||
if (scope === "user" && !temporary) {
|
||||
await this.runNpmCommand(["install", "-g", source.spec]);
|
||||
return;
|
||||
private getNpmInstallArgs(specs: string[], installRoot: string): string[] {
|
||||
const packageManagerName = this.getPackageManagerName();
|
||||
if (packageManagerName === "bun") {
|
||||
return ["install", ...specs, "--cwd", installRoot];
|
||||
}
|
||||
if (packageManagerName === "pnpm") {
|
||||
return ["install", ...specs, "--prefix", installRoot, "--config.strict-dep-builds=false"];
|
||||
}
|
||||
return ["install", ...specs, "--prefix", installRoot];
|
||||
}
|
||||
|
||||
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> {
|
||||
const installRoot = this.getNpmInstallRoot(scope, temporary);
|
||||
this.ensureNpmProject(installRoot);
|
||||
await this.runNpmCommand(["install", source.spec, "--prefix", installRoot]);
|
||||
await this.runNpmCommand(this.getNpmInstallArgs([source.spec], installRoot));
|
||||
}
|
||||
|
||||
private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {
|
||||
if (scope === "user") {
|
||||
await this.runNpmCommand(["uninstall", "-g", source.name]);
|
||||
return;
|
||||
}
|
||||
const installRoot = this.getNpmInstallRoot(scope, false);
|
||||
if (!existsSync(installRoot)) {
|
||||
return;
|
||||
}
|
||||
if (this.getPackageManagerName() === "bun") {
|
||||
await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]);
|
||||
return;
|
||||
}
|
||||
await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]);
|
||||
}
|
||||
|
||||
private async installGit(source: GitSource, scope: SourceScope): Promise<void> {
|
||||
const targetDir = this.getGitInstallPath(source, scope);
|
||||
if (existsSync(targetDir)) {
|
||||
if (source.ref) {
|
||||
await this.ensureGitRef(targetDir, ["fetch", "origin", source.ref], "FETCH_HEAD");
|
||||
return;
|
||||
}
|
||||
const target = await this.getLocalGitUpdateTarget(targetDir);
|
||||
await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
|
||||
return;
|
||||
}
|
||||
const gitRoot = this.getGitInstallRoot(scope);
|
||||
@@ -1737,23 +1767,26 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
const target = await this.getLocalGitUpdateTarget(targetDir);
|
||||
await this.ensureGitRef(targetDir, target.fetchArgs, target.ref);
|
||||
}
|
||||
|
||||
private async ensureGitRef(targetDir: string, fetchArgs: string[], ref: string): Promise<void> {
|
||||
// Fetch only the ref we will reset to, avoiding unrelated branch/tag noise.
|
||||
await this.runCommand("git", target.fetchArgs, { cwd: targetDir });
|
||||
await this.runCommand("git", fetchArgs, { cwd: targetDir });
|
||||
|
||||
const localHead = await this.runCommandCapture("git", ["rev-parse", "HEAD"], {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
const refreshedTargetHead = await this.runCommandCapture("git", ["rev-parse", target.ref], {
|
||||
const targetHead = await this.runCommandCapture("git", ["rev-parse", ref], {
|
||||
cwd: targetDir,
|
||||
timeoutMs: NETWORK_TIMEOUT_MS,
|
||||
});
|
||||
if (localHead.trim() === refreshedTargetHead.trim()) {
|
||||
if (localHead.trim() === targetHead.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.runCommand("git", ["reset", "--hard", target.ref], { cwd: targetDir });
|
||||
await this.runCommand("git", ["reset", "--hard", ref], { cwd: targetDir });
|
||||
|
||||
// Clean untracked files (extensions should be pristine)
|
||||
await this.runCommand("git", ["clean", "-fdx"], { cwd: targetDir });
|
||||
@@ -1810,6 +1843,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (!existsSync(installRoot)) {
|
||||
mkdirSync(installRoot, { recursive: true });
|
||||
}
|
||||
markPathIgnoredByCloudSync(installRoot);
|
||||
this.ensureGitIgnore(installRoot);
|
||||
const packageJsonPath = join(installRoot, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
@@ -1835,7 +1869,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (scope === "project") {
|
||||
return join(this.cwd, CONFIG_DIR_NAME, "npm");
|
||||
}
|
||||
return join(this.getGlobalNpmRoot(), "..");
|
||||
return join(this.agentDir, "npm");
|
||||
}
|
||||
|
||||
private getGlobalNpmRoot(): string {
|
||||
@@ -1844,8 +1878,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||
if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) {
|
||||
return this.globalNpmRoot;
|
||||
}
|
||||
const isBunPackageManager = npmCommand.command === "bun";
|
||||
if (isBunPackageManager) {
|
||||
if (this.getPackageManagerName() === "bun") {
|
||||
const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim();
|
||||
this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules");
|
||||
} else {
|
||||
@@ -1855,14 +1888,45 @@ export class DefaultPackageManager implements PackageManager {
|
||||
return this.globalNpmRoot;
|
||||
}
|
||||
|
||||
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
private getPnpmGlobalPackagePath(packageName: string): string | undefined {
|
||||
if (this.getPackageManagerName() !== "pnpm") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const output = this.runNpmCommandSync(["list", "-g", "--depth", "0", "--json"]);
|
||||
const entries = JSON.parse(output) as Array<{ dependencies?: Record<string, { path?: string }> }>;
|
||||
for (const entry of entries) {
|
||||
const path = entry.dependencies?.[packageName]?.path;
|
||||
if (path) return path;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
if (scope === "temporary") {
|
||||
return join(this.getTemporaryDir("npm"), "node_modules", source.name);
|
||||
}
|
||||
if (scope === "project") {
|
||||
return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name);
|
||||
}
|
||||
return join(this.getGlobalNpmRoot(), source.name);
|
||||
return join(this.agentDir, "npm", "node_modules", source.name);
|
||||
}
|
||||
|
||||
private getLegacyGlobalNpmInstallPath(source: NpmSource): string | undefined {
|
||||
try {
|
||||
return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string {
|
||||
const managedPath = this.getManagedNpmInstallPath(source, scope);
|
||||
if (scope !== "user" || existsSync(managedPath)) {
|
||||
return managedPath;
|
||||
}
|
||||
const legacyPath = this.getLegacyGlobalNpmInstallPath(source);
|
||||
return legacyPath && existsSync(legacyPath) ? legacyPath : managedPath;
|
||||
}
|
||||
|
||||
private getGitInstallPath(source: GitSource, scope: SourceScope): string {
|
||||
@@ -1904,19 +1968,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private resolvePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return getHomeDir();
|
||||
if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1));
|
||||
return resolve(this.cwd, trimmed);
|
||||
return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true });
|
||||
}
|
||||
|
||||
private resolvePathFromBase(input: string, baseDir: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return getHomeDir();
|
||||
if (trimmed.startsWith("~/")) return join(getHomeDir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(getHomeDir(), trimmed.slice(1));
|
||||
return resolve(baseDir, trimmed);
|
||||
return resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true });
|
||||
}
|
||||
|
||||
private collectPackageResources(
|
||||
@@ -2366,11 +2422,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private spawnCommand(command: string, args: string[], options?: { cwd?: string }): ChildProcess {
|
||||
return spawn(command, args, {
|
||||
const env = getEnv();
|
||||
return spawnProcess(command, args, {
|
||||
cwd: options?.cwd,
|
||||
stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit",
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: getEnv(),
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2380,11 +2436,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
options?: { cwd?: string; env?: Record<string, string> },
|
||||
): ChildProcessByStdio<null, Readable, Readable> {
|
||||
const baseEnv = getEnv();
|
||||
return spawn(command, args, {
|
||||
const env = options?.env ? { ...baseEnv, ...options.env } : baseEnv;
|
||||
return spawnProcess(command, args, {
|
||||
cwd: options?.cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: options?.env ? { ...baseEnv, ...options.env } : baseEnv,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2447,11 +2503,11 @@ export class DefaultPackageManager implements PackageManager {
|
||||
}
|
||||
|
||||
private runCommandSync(command: string, args: string[]): string {
|
||||
const result = spawnSync(command, args, {
|
||||
const env = getEnv();
|
||||
const result = spawnProcessSync(command, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
shell: shouldUseWindowsShell(command),
|
||||
env: getEnv(),
|
||||
env,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { basename, dirname, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
/**
|
||||
* Represents a prompt template loaded from a markdown file
|
||||
@@ -37,7 +37,7 @@ export function parseCommandArgs(argsString: string): string[] {
|
||||
}
|
||||
} else if (char === '"' || char === "'") {
|
||||
inQuote = char;
|
||||
} else if (char === " " || char === "\t") {
|
||||
} else if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
@@ -185,19 +185,6 @@ export interface LoadPromptTemplatesOptions {
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return homedir();
|
||||
if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1));
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolvePromptPath(p: string, cwd: string): string {
|
||||
const normalized = normalizePath(p);
|
||||
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all prompt templates from:
|
||||
* 1. Global: agentDir/prompts/
|
||||
@@ -205,14 +192,14 @@ function resolvePromptPath(p: string, cwd: string): string {
|
||||
* 3. Explicit prompt paths
|
||||
*/
|
||||
export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(options.agentDir);
|
||||
const promptPaths = options.promptPaths;
|
||||
const includeDefaults = options.includeDefaults;
|
||||
|
||||
const templates: PromptTemplate[] = [];
|
||||
|
||||
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||
const globalPromptsDir = join(resolvedAgentDir, "prompts");
|
||||
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
||||
|
||||
const isUnderPath = (target: string, root: string): boolean => {
|
||||
@@ -252,7 +239,7 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt
|
||||
|
||||
// 3. Load explicit prompt paths
|
||||
for (const rawPath of promptPaths) {
|
||||
const resolvedPath = resolvePromptPath(rawPath, resolvedCwd);
|
||||
const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true });
|
||||
if (!existsSync(resolvedPath)) {
|
||||
continue;
|
||||
}
|
||||
@@ -282,9 +269,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions): Prompt
|
||||
export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {
|
||||
if (!text.startsWith("/")) return text;
|
||||
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
||||
const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
|
||||
const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);
|
||||
if (!match) return text;
|
||||
|
||||
const templateName = match[1];
|
||||
const argsString = match[2] ?? "";
|
||||
|
||||
const template = templates.find((t) => t.name === templateName);
|
||||
if (template) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { execSync, spawnSync } from "child_process";
|
||||
import { getShellConfig } from "../utils/shell.js";
|
||||
import { getShellConfig } from "../utils/shell.ts";
|
||||
|
||||
// Cache for shell command results (persists for process lifetime)
|
||||
const commandResultCache = new Map<string, string | undefined>();
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import chalk from "chalk";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.ts";
|
||||
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.ts";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js";
|
||||
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
import { canonicalizePath, isLocalPath } from "../utils/paths.js";
|
||||
import { createEventBus, type EventBus } from "./event-bus.js";
|
||||
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.js";
|
||||
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.js";
|
||||
import { DefaultPackageManager, type PathMetadata } from "./package-manager.js";
|
||||
import type { PromptTemplate } from "./prompt-templates.js";
|
||||
import { loadPromptTemplates } from "./prompt-templates.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import type { Skill } from "./skills.js";
|
||||
import { loadSkills } from "./skills.js";
|
||||
import { createSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts";
|
||||
import { createEventBus, type EventBus } from "./event-bus.ts";
|
||||
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts";
|
||||
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts";
|
||||
import { DefaultPackageManager, type PathMetadata } from "./package-manager.ts";
|
||||
import type { PromptTemplate } from "./prompt-templates.ts";
|
||||
import { loadPromptTemplates } from "./prompt-templates.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
import type { Skill } from "./skills.ts";
|
||||
import { loadSkills } from "./skills.ts";
|
||||
import { createSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
export interface ResourceExtensionPaths {
|
||||
skillPaths?: Array<{ path: string; metadata: PathMetadata }>;
|
||||
@@ -77,8 +76,8 @@ export function loadProjectContextFiles(options: {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
}): Array<{ path: string; content: string }> {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(options.agentDir);
|
||||
|
||||
const contextFiles: Array<{ path: string; content: string }> = [];
|
||||
const seenPaths = new Set<string>();
|
||||
@@ -205,8 +204,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private lastThemePaths: string[];
|
||||
|
||||
constructor(options: DefaultResourceLoaderOptions) {
|
||||
this.cwd = options.cwd;
|
||||
this.agentDir = options.agentDir;
|
||||
this.cwd = resolvePath(options.cwd);
|
||||
this.agentDir = resolvePath(options.agentDir);
|
||||
this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir);
|
||||
this.eventBus = options.eventBus ?? createEventBus();
|
||||
this.packageManager = new DefaultPackageManager({
|
||||
@@ -409,8 +408,11 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
for (const p of this.additionalExtensionPaths) {
|
||||
if (isLocalPath(p) && !existsSync(p)) {
|
||||
extensionsResult.errors.push({ path: p, error: `Extension path does not exist: ${p}` });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved)) {
|
||||
extensionsResult.errors.push({ path: resolved, error: `Extension path does not exist: ${resolved}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.extensionsResult = this.extensionsOverride ? this.extensionsOverride(extensionsResult) : extensionsResult;
|
||||
@@ -423,8 +425,11 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastSkillPaths = skillPaths;
|
||||
this.updateSkillsFromPaths(skillPaths, metadataByPath);
|
||||
for (const p of this.additionalSkillPaths) {
|
||||
if (isLocalPath(p) && !existsSync(p) && !this.skillDiagnostics.some((d) => d.path === p)) {
|
||||
this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: p });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.skillDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.skillDiagnostics.push({ type: "error", message: "Skill path does not exist", path: resolved });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,8 +440,15 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastPromptPaths = promptPaths;
|
||||
this.updatePromptsFromPaths(promptPaths, metadataByPath);
|
||||
for (const p of this.additionalPromptTemplatePaths) {
|
||||
if (isLocalPath(p) && !existsSync(p) && !this.promptDiagnostics.some((d) => d.path === p)) {
|
||||
this.promptDiagnostics.push({ type: "error", message: "Prompt template path does not exist", path: p });
|
||||
if (isLocalPath(p)) {
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.promptDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.promptDiagnostics.push({
|
||||
type: "error",
|
||||
message: "Prompt template path does not exist",
|
||||
path: resolved,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,8 +459,9 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastThemePaths = themePaths;
|
||||
this.updateThemesFromPaths(themePaths, metadataByPath);
|
||||
for (const p of this.additionalThemePaths) {
|
||||
if (!existsSync(p) && !this.themeDiagnostics.some((d) => d.path === p)) {
|
||||
this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: p });
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved) && !this.themeDiagnostics.some((d) => d.path === resolved)) {
|
||||
this.themeDiagnostics.push({ type: "error", message: "Theme path does not exist", path: resolved });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,10 +491,15 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private normalizeExtensionPaths(
|
||||
entries: Array<{ path: string; metadata: PathMetadata }>,
|
||||
): Array<{ path: string; metadata: PathMetadata }> {
|
||||
return entries.map((entry) => ({
|
||||
path: this.resolveResourcePath(entry.path),
|
||||
metadata: entry.metadata,
|
||||
}));
|
||||
return entries.map((entry) => {
|
||||
const metadata = entry.metadata.baseDir
|
||||
? { ...entry.metadata, baseDir: this.resolveResourcePath(entry.metadata.baseDir) }
|
||||
: entry.metadata;
|
||||
return {
|
||||
path: this.resolveResourcePath(entry.path),
|
||||
metadata,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private updateSkillsFromPaths(skillPaths: string[], metadataByPath?: Map<string, PathMetadata>): void {
|
||||
@@ -674,16 +692,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
private resolveResourcePath(p: string): string {
|
||||
const trimmed = p.trim();
|
||||
let expanded = trimmed;
|
||||
if (trimmed === "~") {
|
||||
expanded = homedir();
|
||||
} else if (trimmed.startsWith("~/")) {
|
||||
expanded = join(homedir(), trimmed.slice(2));
|
||||
} else if (trimmed.startsWith("~")) {
|
||||
expanded = join(homedir(), trimmed.slice(1));
|
||||
}
|
||||
return resolve(this.cwd, expanded);
|
||||
return resolvePath(p, this.cwd, { trim: true });
|
||||
}
|
||||
|
||||
private loadThemes(
|
||||
@@ -704,7 +713,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
for (const p of paths) {
|
||||
const resolved = resolve(this.cwd, p);
|
||||
const resolved = this.resolveResourcePath(p);
|
||||
if (!existsSync(resolved)) {
|
||||
diagnostics.push({ type: "warning", message: "theme path does not exist", path: resolved });
|
||||
continue;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { join } from "node:path";
|
||||
import { Agent, type AgentMessage, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { clampThinkingLevel, type Message, type Model, streamSimple } from "@earendil-works/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
|
||||
import { AuthStorage } from "./auth-storage.js";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
||||
import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.js";
|
||||
import { convertToLlm } from "./messages.js";
|
||||
import { ModelRegistry } from "./model-registry.js";
|
||||
import { findInitialModel } from "./model-resolver.js";
|
||||
import type { ResourceLoader } from "./resource-loader.js";
|
||||
import { DefaultResourceLoader } from "./resource-loader.js";
|
||||
import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.js";
|
||||
import { time } from "./timings.js";
|
||||
import { getAgentDir } from "../config.ts";
|
||||
import { resolvePath } from "../utils/paths.ts";
|
||||
import { AgentSession } from "./agent-session.ts";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.ts";
|
||||
import { AuthStorage } from "./auth-storage.ts";
|
||||
import { DEFAULT_THINKING_LEVEL } from "./defaults.ts";
|
||||
import type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
||||
import { convertToLlm } from "./messages.ts";
|
||||
import { ModelRegistry } from "./model-registry.ts";
|
||||
import { findInitialModel } from "./model-resolver.ts";
|
||||
import type { ResourceLoader } from "./resource-loader.ts";
|
||||
import { DefaultResourceLoader } from "./resource-loader.ts";
|
||||
import { getDefaultSessionDir, SessionManager } from "./session-manager.ts";
|
||||
import { SettingsManager } from "./settings-manager.ts";
|
||||
import { isInstallTelemetryEnabled } from "./telemetry.ts";
|
||||
import { time } from "./timings.ts";
|
||||
import {
|
||||
createBashTool,
|
||||
createCodingTools,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
createWriteTool,
|
||||
type ToolName,
|
||||
withFileMutationQueue,
|
||||
} from "./tools/index.js";
|
||||
} from "./tools/index.ts";
|
||||
|
||||
export interface CreateAgentSessionOptions {
|
||||
/** Working directory for project-local discovery. Default: process.cwd() */
|
||||
@@ -91,7 +92,7 @@ export interface CreateAgentSessionResult {
|
||||
|
||||
// Re-exports
|
||||
|
||||
export * from "./agent-session-runtime.js";
|
||||
export * from "./agent-session-runtime.ts";
|
||||
export type {
|
||||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
@@ -100,10 +101,10 @@ export type {
|
||||
SlashCommandInfo,
|
||||
SlashCommandSource,
|
||||
ToolDefinition,
|
||||
} from "./extensions/index.js";
|
||||
export type { PromptTemplate } from "./prompt-templates.js";
|
||||
export type { Skill } from "./skills.js";
|
||||
export type { Tool } from "./tools/index.js";
|
||||
} from "./extensions/index.ts";
|
||||
export type { PromptTemplate } from "./prompt-templates.ts";
|
||||
export type { Skill } from "./skills.ts";
|
||||
export type { Tool } from "./tools/index.ts";
|
||||
|
||||
export {
|
||||
withFileMutationQueue,
|
||||
@@ -184,15 +185,15 @@ function getAttributionHeaders(
|
||||
* await loader.reload();
|
||||
* const { session } = await createAgentSession({
|
||||
* model: myModel,
|
||||
* tools: [readTool, bashTool],
|
||||
* tools: ["read", "bash"],
|
||||
* resourceLoader: loader,
|
||||
* sessionManager: SessionManager.inMemory(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
|
||||
const cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();
|
||||
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
||||
const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());
|
||||
const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();
|
||||
let resourceLoader = options.resourceLoader;
|
||||
|
||||
// Use provided or create AuthStorage and ModelRegistry
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import { type AgentMessage, uuidv7 } from "@earendil-works/pi-agent-core";
|
||||
import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
@@ -15,15 +15,15 @@ import {
|
||||
} from "fs";
|
||||
import { readdir, readFile, stat } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
||||
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts";
|
||||
import { normalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import {
|
||||
type BashExecutionMessage,
|
||||
type CustomMessage,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "./messages.js";
|
||||
} from "./messages.ts";
|
||||
|
||||
export const CURRENT_SESSION_VERSION = 3;
|
||||
|
||||
@@ -426,8 +426,10 @@ export function buildSessionContext(
|
||||
* Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
|
||||
*/
|
||||
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
|
||||
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
const sessionDir = join(agentDir, "sessions", safePath);
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
const sessionDir = join(resolvedAgentDir, "sessions", safePath);
|
||||
if (!existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
}
|
||||
@@ -436,9 +438,10 @@ export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultA
|
||||
|
||||
/** Exported for testing */
|
||||
export function loadEntriesFromFile(filePath: string): FileEntry[] {
|
||||
if (!existsSync(filePath)) return [];
|
||||
const resolvedFilePath = normalizePath(filePath);
|
||||
if (!existsSync(resolvedFilePath)) return [];
|
||||
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const content = readFileSync(resolvedFilePath, "utf8");
|
||||
const entries: FileEntry[] = [];
|
||||
const lines = content.trim().split("\n");
|
||||
|
||||
@@ -479,10 +482,11 @@ function isValidSessionFile(filePath: string): boolean {
|
||||
|
||||
/** Exported for testing */
|
||||
export function findMostRecentSession(sessionDir: string): string | null {
|
||||
const resolvedSessionDir = normalizePath(sessionDir);
|
||||
try {
|
||||
const files = readdirSync(sessionDir)
|
||||
const files = readdirSync(resolvedSessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.map((f) => join(sessionDir, f))
|
||||
.map((f) => join(resolvedSessionDir, f))
|
||||
.filter(isValidSessionFile)
|
||||
.map((path) => ({ path, mtime: statSync(path).mtime }))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
@@ -618,6 +622,48 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
|
||||
|
||||
export type SessionListProgress = (loaded: number, total: number) => void;
|
||||
|
||||
const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
|
||||
|
||||
async function buildSessionInfosWithConcurrency(
|
||||
files: string[],
|
||||
onLoaded: () => void,
|
||||
): Promise<(SessionInfo | null)[]> {
|
||||
const results: (SessionInfo | null)[] = new Array(files.length).fill(null);
|
||||
const inFlight = new Set<Promise<void>>();
|
||||
let nextIndex = 0;
|
||||
|
||||
const startNext = (): void => {
|
||||
const index = nextIndex++;
|
||||
const file = files[index];
|
||||
if (!file) return;
|
||||
|
||||
let task: Promise<void>;
|
||||
task = buildSessionInfo(file)
|
||||
.then((info) => {
|
||||
results[index] = info;
|
||||
})
|
||||
.catch(() => {
|
||||
results[index] = null;
|
||||
})
|
||||
.finally(() => {
|
||||
inFlight.delete(task);
|
||||
onLoaded();
|
||||
});
|
||||
inFlight.add(task);
|
||||
};
|
||||
|
||||
while (nextIndex < files.length || inFlight.size > 0) {
|
||||
while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) {
|
||||
startNext();
|
||||
}
|
||||
if (inFlight.size > 0) {
|
||||
await Promise.race(inFlight);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function listSessionsFromDir(
|
||||
dir: string,
|
||||
onProgress?: SessionListProgress,
|
||||
@@ -635,14 +681,10 @@ async function listSessionsFromDir(
|
||||
const total = progressTotal ?? files.length;
|
||||
|
||||
let loaded = 0;
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const info = await buildSessionInfo(file);
|
||||
loaded++;
|
||||
onProgress?.(progressOffset + loaded, total);
|
||||
return info;
|
||||
}),
|
||||
);
|
||||
const results = await buildSessionInfosWithConcurrency(files, () => {
|
||||
loaded++;
|
||||
onProgress?.(progressOffset + loaded, total);
|
||||
});
|
||||
for (const info of results) {
|
||||
if (info) {
|
||||
sessions.push(info);
|
||||
@@ -680,11 +722,11 @@ export class SessionManager {
|
||||
private leafId: string | null = null;
|
||||
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
|
||||
this.cwd = cwd;
|
||||
this.sessionDir = sessionDir;
|
||||
this.cwd = resolvePath(cwd);
|
||||
this.sessionDir = normalizePath(sessionDir);
|
||||
this.persist = persist;
|
||||
if (persist && sessionDir && !existsSync(sessionDir)) {
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
if (persist && this.sessionDir && !existsSync(this.sessionDir)) {
|
||||
mkdirSync(this.sessionDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (sessionFile) {
|
||||
@@ -696,7 +738,7 @@ export class SessionManager {
|
||||
|
||||
/** Switch to a different session file (used for resume and branching) */
|
||||
setSessionFile(sessionFile: string): void {
|
||||
this.sessionFile = resolve(sessionFile);
|
||||
this.sessionFile = resolvePath(sessionFile);
|
||||
if (existsSync(this.sessionFile)) {
|
||||
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
||||
|
||||
@@ -1267,7 +1309,7 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
||||
*/
|
||||
static create(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
@@ -1278,13 +1320,14 @@ export class SessionManager {
|
||||
* @param cwdOverride Optional cwd override instead of the session header cwd.
|
||||
*/
|
||||
static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager {
|
||||
const resolvedPath = resolvePath(path);
|
||||
// Extract cwd from session header if possible, otherwise use process.cwd()
|
||||
const entries = loadEntriesFromFile(path);
|
||||
const entries = loadEntriesFromFile(resolvedPath);
|
||||
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
|
||||
// If no sessionDir provided, derive from file's parent directory
|
||||
const dir = sessionDir ?? resolve(path, "..");
|
||||
return new SessionManager(cwd, dir, path, true);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|
||||
return new SessionManager(cwd, dir, resolvedPath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1293,7 +1336,7 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
||||
*/
|
||||
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
const mostRecent = findMostRecentSession(dir);
|
||||
if (mostRecent) {
|
||||
return new SessionManager(cwd, dir, mostRecent, true);
|
||||
@@ -1314,17 +1357,19 @@ export class SessionManager {
|
||||
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
|
||||
*/
|
||||
static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager {
|
||||
const sourceEntries = loadEntriesFromFile(sourcePath);
|
||||
const resolvedSourcePath = resolvePath(sourcePath);
|
||||
const resolvedTargetCwd = resolvePath(targetCwd);
|
||||
const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
|
||||
if (sourceEntries.length === 0) {
|
||||
throw new Error(`Cannot fork: source session file is empty or invalid: ${sourcePath}`);
|
||||
throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
|
||||
}
|
||||
|
||||
const sourceHeader = sourceEntries.find((e) => e.type === "session") as SessionHeader | undefined;
|
||||
if (!sourceHeader) {
|
||||
throw new Error(`Cannot fork: source session has no header: ${sourcePath}`);
|
||||
throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
|
||||
}
|
||||
|
||||
const dir = sessionDir ?? getDefaultSessionDir(targetCwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
@@ -1341,8 +1386,8 @@ export class SessionManager {
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: newSessionId,
|
||||
timestamp,
|
||||
cwd: targetCwd,
|
||||
parentSession: sourcePath,
|
||||
cwd: resolvedTargetCwd,
|
||||
parentSession: resolvedSourcePath,
|
||||
};
|
||||
appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`);
|
||||
|
||||
@@ -1353,7 +1398,7 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
return new SessionManager(targetCwd, dir, newSessionFile, true);
|
||||
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1363,7 +1408,7 @@ export class SessionManager {
|
||||
* @param onProgress Optional callback for progress updates (loaded, total)
|
||||
*/
|
||||
static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]> {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
||||
const sessions = await listSessionsFromDir(dir, onProgress);
|
||||
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
return sessions;
|
||||
@@ -1401,14 +1446,10 @@ export class SessionManager {
|
||||
const sessions: SessionInfo[] = [];
|
||||
const allFiles = dirFiles.flat();
|
||||
|
||||
const results = await Promise.all(
|
||||
allFiles.map(async (file) => {
|
||||
const info = await buildSessionInfo(file);
|
||||
loaded++;
|
||||
onProgress?.(loaded, totalFiles);
|
||||
return info;
|
||||
}),
|
||||
);
|
||||
const results = await buildSessionInfosWithConcurrency(allFiles, () => {
|
||||
loaded++;
|
||||
onProgress?.(loaded, totalFiles);
|
||||
});
|
||||
|
||||
for (const info of results) {
|
||||
if (info) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Transport } from "@earendil-works/pi-ai";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { dirname, join } from "path";
|
||||
import lockfile from "proper-lockfile";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts";
|
||||
import { normalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.ts";
|
||||
|
||||
export interface CompactionSettings {
|
||||
enabled?: boolean; // default: true
|
||||
@@ -110,6 +111,7 @@ export interface Settings {
|
||||
markdown?: MarkdownSettings;
|
||||
warnings?: WarningSettings;
|
||||
sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)
|
||||
httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it
|
||||
}
|
||||
|
||||
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
||||
@@ -159,8 +161,10 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
private projectSettingsPath: string;
|
||||
|
||||
constructor(cwd: string, agentDir: string) {
|
||||
this.globalSettingsPath = join(agentDir, "settings.json");
|
||||
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
|
||||
const resolvedCwd = resolvePath(cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir);
|
||||
this.globalSettingsPath = join(resolvedAgentDir, "settings.json");
|
||||
this.projectSettingsPath = join(resolvedCwd, CONFIG_DIR_NAME, "settings.json");
|
||||
}
|
||||
|
||||
private acquireLockSyncWithRetry(path: string): () => void {
|
||||
@@ -575,16 +579,7 @@ export class SettingsManager {
|
||||
|
||||
getSessionDir(): string | undefined {
|
||||
const sessionDir = this.settings.sessionDir;
|
||||
if (!sessionDir) {
|
||||
return sessionDir;
|
||||
}
|
||||
if (sessionDir === "~") {
|
||||
return homedir();
|
||||
}
|
||||
if (sessionDir.startsWith("~/")) {
|
||||
return join(homedir(), sessionDir.slice(2));
|
||||
}
|
||||
return sessionDir;
|
||||
return sessionDir ? normalizePath(sessionDir) : sessionDir;
|
||||
}
|
||||
|
||||
getDefaultProvider(): string | undefined {
|
||||
@@ -726,6 +721,27 @@ export class SettingsManager {
|
||||
};
|
||||
}
|
||||
|
||||
getHttpIdleTimeoutMs(): number {
|
||||
const value = this.settings.httpIdleTimeoutMs;
|
||||
const timeoutMs = parseHttpIdleTimeoutMs(value);
|
||||
if (timeoutMs !== undefined) {
|
||||
return timeoutMs;
|
||||
}
|
||||
if (value !== undefined) {
|
||||
throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(value)}`);
|
||||
}
|
||||
return DEFAULT_HTTP_IDLE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
setHttpIdleTimeoutMs(timeoutMs: number): void {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
||||
throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(timeoutMs)}`);
|
||||
}
|
||||
this.globalSettings.httpIdleTimeoutMs = Math.floor(timeoutMs);
|
||||
this.markModified("httpIdleTimeoutMs");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {
|
||||
return {
|
||||
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import ignore from "ignore";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { canonicalizePath } from "../utils/paths.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
import { basename, dirname, join, relative, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.ts";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.ts";
|
||||
import { canonicalizePath, resolvePath } from "../utils/paths.ts";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.ts";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
|
||||
|
||||
/** Max name length per spec */
|
||||
const MAX_NAME_LENGTH = 64;
|
||||
@@ -90,13 +89,9 @@ export interface LoadSkillsResult {
|
||||
* Validate skill name per Agent Skills spec.
|
||||
* Returns array of validation error messages (empty if valid).
|
||||
*/
|
||||
function validateName(name: string, parentDirName: string): string[] {
|
||||
function validateName(name: string): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (name !== parentDirName) {
|
||||
errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
||||
}
|
||||
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
||||
}
|
||||
@@ -301,7 +296,7 @@ function loadSkillFromFile(
|
||||
const name = frontmatter.name || parentDirName;
|
||||
|
||||
// Validate name
|
||||
const nameErrors = validateName(name, parentDirName);
|
||||
const nameErrors = validateName(name);
|
||||
for (const error of nameErrors) {
|
||||
diagnostics.push({ type: "warning", message: error, path: filePath });
|
||||
}
|
||||
@@ -385,28 +380,16 @@ export interface LoadSkillsOptions {
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === "~") return homedir();
|
||||
if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
|
||||
if (trimmed.startsWith("~")) return join(homedir(), trimmed.slice(1));
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveSkillPath(p: string, cwd: string): string {
|
||||
const normalized = normalizePath(p);
|
||||
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skills from all configured locations.
|
||||
* Returns skills and any validation diagnostics.
|
||||
*/
|
||||
export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
const { cwd, agentDir, skillPaths, includeDefaults } = options;
|
||||
const { agentDir, skillPaths, includeDefaults } = options;
|
||||
|
||||
// Resolve agentDir - if not provided, use default from config
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
const resolvedCwd = resolvePath(options.cwd);
|
||||
const resolvedAgentDir = resolvePath(agentDir ?? getAgentDir());
|
||||
|
||||
const skillMap = new Map<string, Skill>();
|
||||
const realPathSet = new Set<string>();
|
||||
@@ -446,11 +429,11 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
|
||||
if (includeDefaults) {
|
||||
addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
||||
addSkills(loadSkillsFromDirInternal(resolve(resolvedCwd, CONFIG_DIR_NAME, "skills"), "project", true));
|
||||
}
|
||||
|
||||
const userSkillsDir = join(resolvedAgentDir, "skills");
|
||||
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
||||
const projectSkillsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "skills");
|
||||
|
||||
const isUnderPath = (target: string, root: string): boolean => {
|
||||
const normalizedRoot = resolve(root);
|
||||
@@ -470,7 +453,7 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
};
|
||||
|
||||
for (const rawPath of skillPaths) {
|
||||
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
||||
const resolvedPath = resolvePath(rawPath, resolvedCwd, { trim: true });
|
||||
if (!existsSync(resolvedPath)) {
|
||||
allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { APP_NAME } from "../config.js";
|
||||
import type { SourceInfo } from "./source-info.js";
|
||||
import { APP_NAME } from "../config.ts";
|
||||
import type { SourceInfo } from "./source-info.ts";
|
||||
|
||||
export type SlashCommandSource = "extension" | "prompt" | "skill";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PathMetadata } from "./package-manager.js";
|
||||
import type { PathMetadata } from "./package-manager.ts";
|
||||
|
||||
export type SourceScope = "user" | "project" | "temporary";
|
||||
export type SourceOrigin = "package" | "top-level";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* System prompt construction and project context loading
|
||||
*/
|
||||
|
||||
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skills.js";
|
||||
import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts";
|
||||
import { formatSkillsForPrompt, type Skill } from "./skills.ts";
|
||||
|
||||
export interface BuildSystemPromptOptions {
|
||||
/** Custom system prompt (replaces default). */
|
||||
@@ -59,11 +59,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
|
||||
|
||||
// Append project context files
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "\n\n<project_context>\n\n";
|
||||
prompt += "Project-specific instructions and guidelines:\n\n";
|
||||
for (const { path: filePath, content } of contextFiles) {
|
||||
prompt += `## ${filePath}\n\n${content}\n\n`;
|
||||
prompt += `<project_instructions path="${filePath}">\n${content}\n</project_instructions>\n\n`;
|
||||
}
|
||||
prompt += "</project_context>\n";
|
||||
}
|
||||
|
||||
// Append skills section (only if read tool is available)
|
||||
@@ -142,6 +143,7 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi
|
||||
- Main documentation: ${readmePath}
|
||||
- Additional docs: ${docsPath}
|
||||
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
||||
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
||||
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
|
||||
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
||||
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
||||
@@ -152,11 +154,12 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi
|
||||
|
||||
// Append project context files
|
||||
if (contextFiles.length > 0) {
|
||||
prompt += "\n\n# Project Context\n\n";
|
||||
prompt += "\n\n<project_context>\n\n";
|
||||
prompt += "Project-specific instructions and guidelines:\n\n";
|
||||
for (const { path: filePath, content } of contextFiles) {
|
||||
prompt += `## ${filePath}\n\n${content}\n\n`;
|
||||
prompt += `<project_instructions path="${filePath}">\n${content}\n</project_instructions>\n\n`;
|
||||
}
|
||||
prompt += "</project_context>\n";
|
||||
}
|
||||
|
||||
// Append skills section (only if read tool is available)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.ts";
|
||||
|
||||
function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
|
||||
@@ -3,22 +3,22 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
import { spawn } from "child_process";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
||||
import { theme } from "../../modes/interactive/theme/theme.js";
|
||||
import { waitForChildProcess } from "../../utils/child-process.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.ts";
|
||||
import { theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { waitForChildProcess } from "../../utils/child-process.ts";
|
||||
import {
|
||||
getShellConfig,
|
||||
getShellEnv,
|
||||
killProcessTree,
|
||||
trackDetachedChildPid,
|
||||
untrackDetachedChildPid,
|
||||
} from "../../utils/shell.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { OutputAccumulator } from "./output-accumulator.js";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js";
|
||||
} from "../../utils/shell.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { OutputAccumulator } from "./output-accumulator.ts";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts";
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
@@ -76,6 +76,7 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas
|
||||
detached: process.platform !== "win32",
|
||||
env: env ?? getShellEnv(),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
if (child.pid) trackDetachedChildPid(child.pid);
|
||||
let timedOut = false;
|
||||
@@ -199,7 +200,15 @@ function rebuildBashResultRenderComponent(
|
||||
const state = component.state;
|
||||
component.clear();
|
||||
|
||||
const output = getTextOutput(result as any, showImages).trim();
|
||||
let output = getTextOutput(result as any, showImages).trim();
|
||||
const truncation = result.details?.truncation;
|
||||
const fullOutputPath = result.details?.fullOutputPath;
|
||||
if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) {
|
||||
const footerStart = output.lastIndexOf("\n\n[");
|
||||
if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) {
|
||||
output = output.slice(0, footerStart).trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
const styledOutput = output
|
||||
@@ -235,8 +244,6 @@ function rebuildBashResultRenderComponent(
|
||||
}
|
||||
}
|
||||
|
||||
const truncation = result.details?.truncation;
|
||||
const fullOutputPath = result.details?.fullOutputPath;
|
||||
if (truncation?.truncated || fullOutputPath) {
|
||||
const warnings: string[] = [];
|
||||
if (fullOutputPath) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as Diff from "diff";
|
||||
import { constants } from "fs";
|
||||
import { access, readFile } from "fs/promises";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
|
||||
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
||||
const crlfIdx = content.indexOf("\r\n");
|
||||
@@ -259,8 +259,16 @@ export function applyEditsToNormalizedContent(
|
||||
return { baseContent, newContent };
|
||||
}
|
||||
|
||||
/** Generate a standard unified patch. */
|
||||
export function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {
|
||||
return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
|
||||
context: contextLines,
|
||||
headerOptions: Diff.FILE_HEADERS_ONLY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unified diff string with line numbers and context.
|
||||
* Generate a display-oriented diff string with line numbers and context.
|
||||
* Returns both the diff string and the first changed line number (in the new file).
|
||||
*/
|
||||
export function generateDiffString(
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.js";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.ts";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import {
|
||||
applyEditsToNormalizedContent,
|
||||
computeEditsDiff,
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
type EditDiffError,
|
||||
type EditDiffResult,
|
||||
generateDiffString,
|
||||
generateUnifiedPatch,
|
||||
normalizeToLF,
|
||||
restoreLineEndings,
|
||||
stripBom,
|
||||
} from "./edit-diff.js";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
} from "./edit-diff.ts";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
type EditPreview = EditDiffResult | EditDiffError;
|
||||
|
||||
@@ -57,8 +58,10 @@ type LegacyEditToolInput = EditToolInput & {
|
||||
};
|
||||
|
||||
export interface EditToolDetails {
|
||||
/** Unified diff of the changes made */
|
||||
/** Display-oriented diff of the changes made */
|
||||
diff: string;
|
||||
/** Standard unified patch of the changes made */
|
||||
patch: string;
|
||||
/** Line number of the first change in the new file (for editor navigation) */
|
||||
firstChangedLine?: number;
|
||||
}
|
||||
@@ -190,7 +193,7 @@ function getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path
|
||||
|
||||
function formatEditCall(
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const invalidArg = invalidArgText(theme);
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -203,7 +206,7 @@ function formatEditResult(
|
||||
args: RenderableEditArgs | undefined,
|
||||
preview: EditPreview | undefined,
|
||||
result: EditToolResultLike,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
isError: boolean,
|
||||
): string | undefined {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -231,7 +234,7 @@ function formatEditResult(
|
||||
function getEditHeaderBg(
|
||||
preview: EditPreview | undefined,
|
||||
settledError: boolean | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): (text: string) => string {
|
||||
if (preview) {
|
||||
if ("error" in preview) {
|
||||
@@ -248,7 +251,7 @@ function getEditHeaderBg(
|
||||
function buildEditCallComponent(
|
||||
component: EditCallRenderComponent,
|
||||
args: RenderableEditArgs | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): EditCallRenderComponent {
|
||||
component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
|
||||
component.clear();
|
||||
@@ -394,6 +397,7 @@ export function createEditToolDefinition(
|
||||
}
|
||||
|
||||
const diffResult = generateDiffString(baseContent, newContent);
|
||||
const patch = generateUnifiedPatch(path, baseContent, newContent);
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
@@ -401,7 +405,7 @@ export function createEditToolDefinition(
|
||||
text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
|
||||
},
|
||||
],
|
||||
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine },
|
||||
details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// Clean up abort handler.
|
||||
|
||||
@@ -5,13 +5,13 @@ import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
function toPosixPath(value: string): string {
|
||||
return value.split(path.sep).join("/");
|
||||
@@ -58,7 +58,7 @@ export interface FindToolOptions {
|
||||
|
||||
function formatFindCall(
|
||||
args: { pattern: string; path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
@@ -82,7 +82,7 @@ function formatFindResult(
|
||||
details?: FindToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -5,12 +5,12 @@ import { spawn } from "child_process";
|
||||
import { readFileSync, statSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import {
|
||||
DEFAULT_MAX_BYTES,
|
||||
formatSize,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type TruncationResult,
|
||||
truncateHead,
|
||||
truncateLine,
|
||||
} from "./truncate.js";
|
||||
} from "./truncate.ts";
|
||||
|
||||
const grepSchema = Type.Object({
|
||||
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
||||
@@ -66,7 +66,7 @@ export interface GrepToolOptions {
|
||||
|
||||
function formatGrepCall(
|
||||
args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const pattern = str(args?.pattern);
|
||||
const rawPath = str(args?.path);
|
||||
@@ -90,7 +90,7 @@ function formatGrepResult(
|
||||
details?: GrepToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -8,7 +8,7 @@ export {
|
||||
createBashTool,
|
||||
createBashToolDefinition,
|
||||
createLocalBashOperations,
|
||||
} from "./bash.js";
|
||||
} from "./bash.ts";
|
||||
export {
|
||||
createEditTool,
|
||||
createEditToolDefinition,
|
||||
@@ -16,8 +16,8 @@ export {
|
||||
type EditToolDetails,
|
||||
type EditToolInput,
|
||||
type EditToolOptions,
|
||||
} from "./edit.js";
|
||||
export { withFileMutationQueue } from "./file-mutation-queue.js";
|
||||
} from "./edit.ts";
|
||||
export { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
export {
|
||||
createFindTool,
|
||||
createFindToolDefinition,
|
||||
@@ -25,7 +25,7 @@ export {
|
||||
type FindToolDetails,
|
||||
type FindToolInput,
|
||||
type FindToolOptions,
|
||||
} from "./find.js";
|
||||
} from "./find.ts";
|
||||
export {
|
||||
createGrepTool,
|
||||
createGrepToolDefinition,
|
||||
@@ -33,7 +33,7 @@ export {
|
||||
type GrepToolDetails,
|
||||
type GrepToolInput,
|
||||
type GrepToolOptions,
|
||||
} from "./grep.js";
|
||||
} from "./grep.ts";
|
||||
export {
|
||||
createLsTool,
|
||||
createLsToolDefinition,
|
||||
@@ -41,7 +41,7 @@ export {
|
||||
type LsToolDetails,
|
||||
type LsToolInput,
|
||||
type LsToolOptions,
|
||||
} from "./ls.js";
|
||||
} from "./ls.ts";
|
||||
export {
|
||||
createReadTool,
|
||||
createReadToolDefinition,
|
||||
@@ -49,7 +49,7 @@ export {
|
||||
type ReadToolDetails,
|
||||
type ReadToolInput,
|
||||
type ReadToolOptions,
|
||||
} from "./read.js";
|
||||
} from "./read.ts";
|
||||
export {
|
||||
DEFAULT_MAX_BYTES,
|
||||
DEFAULT_MAX_LINES,
|
||||
@@ -59,24 +59,24 @@ export {
|
||||
truncateHead,
|
||||
truncateLine,
|
||||
truncateTail,
|
||||
} from "./truncate.js";
|
||||
} from "./truncate.ts";
|
||||
export {
|
||||
createWriteTool,
|
||||
createWriteToolDefinition,
|
||||
type WriteOperations,
|
||||
type WriteToolInput,
|
||||
type WriteToolOptions,
|
||||
} from "./write.js";
|
||||
} from "./write.ts";
|
||||
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.js";
|
||||
import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.js";
|
||||
import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.js";
|
||||
import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.js";
|
||||
import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.js";
|
||||
import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.js";
|
||||
import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.js";
|
||||
import type { ToolDefinition } from "../extensions/types.ts";
|
||||
import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.ts";
|
||||
import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.ts";
|
||||
import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.ts";
|
||||
import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.ts";
|
||||
import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.ts";
|
||||
import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.ts";
|
||||
import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.ts";
|
||||
|
||||
export type Tool = AgentTool<any>;
|
||||
export type ToolDef = ToolDefinition<any, any>;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Text } from "@earendil-works/pi-tui";
|
||||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import nodePath from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
const lsSchema = Type.Object({
|
||||
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
||||
@@ -50,7 +50,7 @@ export interface LsToolOptions {
|
||||
|
||||
function formatLsCall(
|
||||
args: { path?: string; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string {
|
||||
const rawPath = str(args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
||||
@@ -69,7 +69,7 @@ function formatLsResult(
|
||||
details?: LsToolDetails;
|
||||
},
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
showImages: boolean,
|
||||
): string {
|
||||
const output = getTextOutput(result, showImages).trim();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.ts";
|
||||
|
||||
export interface OutputAccumulatorOptions {
|
||||
maxLines?: number;
|
||||
@@ -45,8 +45,10 @@ export class OutputAccumulator {
|
||||
private tailStartsAtLineBoundary = true;
|
||||
private totalRawBytes = 0;
|
||||
private totalDecodedBytes = 0;
|
||||
private totalLines = 1;
|
||||
private completedLines = 0;
|
||||
private totalLines = 0;
|
||||
private currentLineBytes = 0;
|
||||
private hasOpenLine = false;
|
||||
private finished = false;
|
||||
|
||||
private tempFilePath: string | undefined;
|
||||
@@ -164,10 +166,14 @@ export class OutputAccumulator {
|
||||
}
|
||||
if (newlines === 0) {
|
||||
this.currentLineBytes += bytes;
|
||||
this.hasOpenLine = true;
|
||||
} else {
|
||||
this.totalLines += newlines;
|
||||
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
|
||||
this.completedLines += newlines;
|
||||
const tail = text.slice(lastNewline + 1);
|
||||
this.currentLineBytes = byteLength(tail);
|
||||
this.hasOpenLine = tail.length > 0;
|
||||
}
|
||||
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
|
||||
}
|
||||
|
||||
private trimTail(): void {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { accessSync, constants } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import { isAbsolute, resolve as resolvePath } from "node:path";
|
||||
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
||||
|
||||
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||
const NARROW_NO_BREAK_SPACE = "\u202F";
|
||||
function normalizeUnicodeSpaces(str: string): string {
|
||||
return str.replace(UNICODE_SPACES, " ");
|
||||
}
|
||||
|
||||
function tryMacOSScreenshotPath(filePath: string): string {
|
||||
return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
|
||||
@@ -32,19 +27,8 @@ function fileExists(filePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAtPrefix(filePath: string): string {
|
||||
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
||||
}
|
||||
|
||||
export function expandPath(filePath: string): string {
|
||||
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
||||
if (normalized === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
if (normalized.startsWith("~/")) {
|
||||
return os.homedir() + normalized.slice(1);
|
||||
}
|
||||
return normalized;
|
||||
return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +36,7 @@ export function expandPath(filePath: string): string {
|
||||
* Handles ~ expansion and absolute paths.
|
||||
*/
|
||||
export function resolveToCwd(filePath: string, cwd: string): string {
|
||||
const expanded = expandPath(filePath);
|
||||
if (isAbsolute(expanded)) {
|
||||
return expanded;
|
||||
}
|
||||
return resolvePath(cwd, expanded);
|
||||
return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
||||
}
|
||||
|
||||
export function resolveReadPath(filePath: string, cwd: string): string {
|
||||
|
||||
@@ -5,17 +5,17 @@ import { Text } from "@earendil-works/pi-tui";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { getReadmePath } from "../../config.js";
|
||||
import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
||||
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveReadPath } from "./path-utils.js";
|
||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||
import { getReadmePath } from "../../config.ts";
|
||||
import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.ts";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.ts";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
|
||||
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { resolveReadPath } from "./path-utils.ts";
|
||||
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
||||
|
||||
const readSchema = Type.Object({
|
||||
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as os from "node:os";
|
||||
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
||||
import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
||||
import { stripAnsi } from "../../utils/ansi.ts";
|
||||
import { sanitizeBinaryOutput } from "../../utils/shell.ts";
|
||||
|
||||
export function shortenPath(path: unknown): string {
|
||||
if (typeof path !== "string") return "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import type { ExtensionContext, ToolDefinition } from "../extensions/types.js";
|
||||
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
||||
|
||||
/** Wrap a ToolDefinition into an AgentTool for the core runtime. */
|
||||
export function wrapToolDefinition<TDetails = unknown>(
|
||||
|
||||
@@ -44,6 +44,17 @@ export interface TruncationOptions {
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
function splitLinesForCounting(content: string): string[] {
|
||||
if (content.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lines = content.split("\n");
|
||||
if (content.endsWith("\n")) {
|
||||
lines.pop();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes as human-readable size.
|
||||
*/
|
||||
@@ -69,7 +80,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
||||
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
|
||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const lines = splitLinesForCounting(content);
|
||||
const totalLines = lines.length;
|
||||
|
||||
// Check if no truncation needed
|
||||
@@ -159,7 +170,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
||||
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
|
||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const lines = splitLinesForCounting(content);
|
||||
const totalLines = lines.length;
|
||||
|
||||
// Check if no truncation needed
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Container, Text } from "@earendil-works/pi-tui";
|
||||
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { dirname } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.ts";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.ts";
|
||||
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
||||
import { resolveToCwd } from "./path-utils.ts";
|
||||
import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.ts";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
||||
|
||||
const writeSchema = Type.Object({
|
||||
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
||||
@@ -131,7 +131,7 @@ function trimTrailingEmptyLines(lines: string[]): string[] {
|
||||
function formatWriteCall(
|
||||
args: { path?: string; file_path?: string; content?: string } | undefined,
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
cache: WriteHighlightCache | undefined,
|
||||
): string {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
@@ -163,7 +163,7 @@ function formatWriteCall(
|
||||
|
||||
function formatWriteResult(
|
||||
result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.ts").theme,
|
||||
): string | undefined {
|
||||
if (!result.isError) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Core session management
|
||||
|
||||
// Config paths
|
||||
export { getAgentDir, VERSION } from "./config.js";
|
||||
export { getAgentDir, VERSION } from "./config.ts";
|
||||
export {
|
||||
AgentSession,
|
||||
type AgentSessionConfig,
|
||||
@@ -12,7 +12,7 @@ export {
|
||||
type PromptOptions,
|
||||
parseSkillBlock,
|
||||
type SessionStats,
|
||||
} from "./core/agent-session.js";
|
||||
} from "./core/agent-session.ts";
|
||||
// Auth and model registry
|
||||
export {
|
||||
type ApiKeyCredential,
|
||||
@@ -23,7 +23,7 @@ export {
|
||||
FileAuthStorageBackend,
|
||||
InMemoryAuthStorageBackend,
|
||||
type OAuthCredential,
|
||||
} from "./core/auth-storage.js";
|
||||
} from "./core/auth-storage.ts";
|
||||
// Compaction
|
||||
export {
|
||||
type BranchPreparation,
|
||||
@@ -46,8 +46,8 @@ export {
|
||||
prepareBranchEntries,
|
||||
serializeConversation,
|
||||
shouldCompact,
|
||||
} from "./core/compaction/index.js";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.js";
|
||||
} from "./core/compaction/index.ts";
|
||||
export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts";
|
||||
// Extension system
|
||||
export type {
|
||||
AgentEndEvent,
|
||||
@@ -128,7 +128,7 @@ export type {
|
||||
WidgetPlacement,
|
||||
WorkingIndicatorOptions,
|
||||
WriteToolCallEvent,
|
||||
} from "./core/extensions/index.js";
|
||||
} from "./core/extensions/index.ts";
|
||||
export {
|
||||
createExtensionRuntime,
|
||||
defineTool,
|
||||
@@ -144,11 +144,11 @@ export {
|
||||
isWriteToolResult,
|
||||
wrapRegisteredTool,
|
||||
wrapRegisteredTools,
|
||||
} from "./core/extensions/index.js";
|
||||
} from "./core/extensions/index.ts";
|
||||
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
|
||||
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
|
||||
export { convertToLlm } from "./core/messages.js";
|
||||
export { ModelRegistry } from "./core/model-registry.js";
|
||||
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
|
||||
export { convertToLlm } from "./core/messages.ts";
|
||||
export { ModelRegistry } from "./core/model-registry.ts";
|
||||
export type {
|
||||
PackageManager,
|
||||
PathMetadata,
|
||||
@@ -156,10 +156,10 @@ export type {
|
||||
ProgressEvent,
|
||||
ResolvedPaths,
|
||||
ResolvedResource,
|
||||
} from "./core/package-manager.js";
|
||||
export { DefaultPackageManager } from "./core/package-manager.js";
|
||||
export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.js";
|
||||
export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.js";
|
||||
} from "./core/package-manager.ts";
|
||||
export { DefaultPackageManager } from "./core/package-manager.ts";
|
||||
export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.ts";
|
||||
export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts";
|
||||
// SDK for programmatic usage
|
||||
export {
|
||||
AgentSessionRuntime,
|
||||
@@ -187,7 +187,7 @@ export {
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
type PromptTemplate,
|
||||
} from "./core/sdk.js";
|
||||
} from "./core/sdk.ts";
|
||||
export {
|
||||
type BranchSummaryEntry,
|
||||
buildSessionContext,
|
||||
@@ -210,14 +210,14 @@ export {
|
||||
SessionManager,
|
||||
type SessionMessageEntry,
|
||||
type ThinkingLevelChangeEntry,
|
||||
} from "./core/session-manager.js";
|
||||
} from "./core/session-manager.ts";
|
||||
export {
|
||||
type CompactionSettings,
|
||||
type ImageSettings,
|
||||
type PackageSource,
|
||||
type RetrySettings,
|
||||
SettingsManager,
|
||||
} from "./core/settings-manager.js";
|
||||
} from "./core/settings-manager.ts";
|
||||
// Skills
|
||||
export {
|
||||
formatSkillsForPrompt,
|
||||
@@ -227,8 +227,8 @@ export {
|
||||
loadSkillsFromDir,
|
||||
type Skill,
|
||||
type SkillFrontmatter,
|
||||
} from "./core/skills.js";
|
||||
export { createSyntheticSourceInfo } from "./core/source-info.js";
|
||||
} from "./core/skills.ts";
|
||||
export { createSyntheticSourceInfo } from "./core/source-info.ts";
|
||||
// Tools
|
||||
export {
|
||||
type BashOperations,
|
||||
@@ -278,9 +278,9 @@ export {
|
||||
type WriteToolInput,
|
||||
type WriteToolOptions,
|
||||
withFileMutationQueue,
|
||||
} from "./core/tools/index.js";
|
||||
} from "./core/tools/index.ts";
|
||||
// Main entry point
|
||||
export { type MainOptions, main } from "./main.js";
|
||||
export { type MainOptions, main } from "./main.ts";
|
||||
// Run modes for programmatic SDK usage
|
||||
export {
|
||||
InteractiveMode,
|
||||
@@ -295,7 +295,7 @@ export {
|
||||
type RpcSessionState,
|
||||
runPrintMode,
|
||||
runRpcMode,
|
||||
} from "./modes/index.js";
|
||||
} from "./modes/index.ts";
|
||||
// UI components for extensions
|
||||
export {
|
||||
ArminComponent,
|
||||
@@ -334,7 +334,7 @@ export {
|
||||
UserMessageComponent,
|
||||
UserMessageSelectorComponent,
|
||||
type VisualTruncateResult,
|
||||
} from "./modes/interactive/components/index.js";
|
||||
} from "./modes/interactive/components/index.ts";
|
||||
// Theme utilities for custom tools and extensions
|
||||
export {
|
||||
getLanguageFromPath,
|
||||
@@ -345,9 +345,10 @@ export {
|
||||
initTheme,
|
||||
Theme,
|
||||
type ThemeColor,
|
||||
} from "./modes/interactive/theme/theme.js";
|
||||
} from "./modes/interactive/theme/theme.ts";
|
||||
// Clipboard utilities
|
||||
export { copyToClipboard } from "./utils/clipboard.js";
|
||||
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
|
||||
export { copyToClipboard } from "./utils/clipboard.ts";
|
||||
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts";
|
||||
export { formatDimensionNote, type ResizedImage, resizeImage } from "./utils/image-resize.ts";
|
||||
// Shell utilities
|
||||
export { getShellConfig } from "./utils/shell.js";
|
||||
export { getShellConfig } from "./utils/shell.ts";
|
||||
|
||||
@@ -5,47 +5,48 @@
|
||||
* createAgentSession() options. The SDK does the heavy lifting.
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { type ImageContent, modelsAreEqual } from "@earendil-works/pi-ai";
|
||||
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js";
|
||||
import { processFileArguments } from "./cli/file-processor.js";
|
||||
import { buildInitialMessage } from "./cli/initial-message.js";
|
||||
import { listModels } from "./cli/list-models.js";
|
||||
import { selectSession } from "./cli/session-picker.js";
|
||||
import { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from "./config.js";
|
||||
import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.js";
|
||||
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.ts";
|
||||
import { processFileArguments } from "./cli/file-processor.ts";
|
||||
import { buildInitialMessage } from "./cli/initial-message.ts";
|
||||
import { listModels } from "./cli/list-models.ts";
|
||||
import { selectSession } from "./cli/session-picker.ts";
|
||||
import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.ts";
|
||||
import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.ts";
|
||||
import {
|
||||
type AgentSessionRuntimeDiagnostic,
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./core/agent-session-services.js";
|
||||
import { formatNoModelsAvailableMessage } from "./core/auth-guidance.js";
|
||||
import { AuthStorage } from "./core/auth-storage.js";
|
||||
import { exportFromFile } from "./core/export-html/index.js";
|
||||
import type { ExtensionFactory } from "./core/extensions/types.js";
|
||||
import { KeybindingsManager } from "./core/keybindings.js";
|
||||
import type { ModelRegistry } from "./core/model-registry.js";
|
||||
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||
import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
|
||||
import type { CreateAgentSessionOptions } from "./core/sdk.js";
|
||||
} from "./core/agent-session-services.ts";
|
||||
import { formatNoModelsAvailableMessage } from "./core/auth-guidance.ts";
|
||||
import { AuthStorage } from "./core/auth-storage.ts";
|
||||
import { exportFromFile } from "./core/export-html/index.ts";
|
||||
import type { ExtensionFactory } from "./core/extensions/types.ts";
|
||||
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
|
||||
import { KeybindingsManager } from "./core/keybindings.ts";
|
||||
import type { ModelRegistry } from "./core/model-registry.ts";
|
||||
import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.ts";
|
||||
import { restoreStdout, takeOverStdout } from "./core/output-guard.ts";
|
||||
import type { CreateAgentSessionOptions } from "./core/sdk.ts";
|
||||
import {
|
||||
formatMissingSessionCwdPrompt,
|
||||
getMissingSessionCwdIssue,
|
||||
MissingSessionCwdError,
|
||||
type SessionCwdIssue,
|
||||
} from "./core/session-cwd.js";
|
||||
import { SessionManager } from "./core/session-manager.js";
|
||||
import { SettingsManager } from "./core/settings-manager.js";
|
||||
import { printTimings, resetTimings, time } from "./core/timings.js";
|
||||
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||
import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
|
||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
|
||||
import { isLocalPath } from "./utils/paths.js";
|
||||
} from "./core/session-cwd.ts";
|
||||
import { SessionManager } from "./core/session-manager.ts";
|
||||
import { SettingsManager } from "./core/settings-manager.ts";
|
||||
import { printTimings, resetTimings, time } from "./core/timings.ts";
|
||||
import { runMigrations, showDeprecationWarnings } from "./migrations.ts";
|
||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts";
|
||||
import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.ts";
|
||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts";
|
||||
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
|
||||
import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts";
|
||||
import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts";
|
||||
|
||||
/**
|
||||
* Read all content from piped stdin.
|
||||
@@ -145,9 +146,9 @@ type ResolvedSession =
|
||||
* If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
|
||||
*/
|
||||
async function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {
|
||||
// If it looks like a file path, use as-is
|
||||
// If it looks like a file path, resolve it before handing it to the session manager.
|
||||
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
||||
return { type: "path", path: sessionArg };
|
||||
return { type: "path", path: resolvePath(sessionArg, cwd) };
|
||||
}
|
||||
|
||||
// Try to match as session ID in current project first
|
||||
@@ -379,7 +380,7 @@ function buildSessionOptions(
|
||||
}
|
||||
|
||||
function resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {
|
||||
return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));
|
||||
return paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));
|
||||
}
|
||||
|
||||
async function promptForMissingSessionCwd(
|
||||
@@ -428,6 +429,10 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
process.env.PI_SKIP_VERSION_CHECK = "1";
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
cleanupWindowsSelfUpdateQuarantine(getPackageDir());
|
||||
}
|
||||
|
||||
if (await handlePackageCommand(args)) {
|
||||
return;
|
||||
}
|
||||
@@ -495,7 +500,7 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
// sessionDir lookup during session selection.
|
||||
const envSessionDir = process.env[ENV_SESSION_DIR];
|
||||
const sessionDir =
|
||||
parsed.sessionDir ??
|
||||
(parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??
|
||||
(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??
|
||||
startupSettingsManager.getSessionDir();
|
||||
let sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);
|
||||
@@ -612,6 +617,7 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
});
|
||||
const { services, session, modelFallbackMessage } = runtime;
|
||||
const { settingsManager, modelRegistry, resourceLoader } = services;
|
||||
configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
|
||||
|
||||
if (parsed.help) {
|
||||
const extensionFlags = resourceLoader
|
||||
@@ -651,7 +657,6 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
await showDeprecationWarnings(deprecationWarnings);
|
||||
}
|
||||
|
||||
const scopedModels = [...session.scopedModels];
|
||||
time("resolveModelScope");
|
||||
reportDiagnostics(runtime.diagnostics);
|
||||
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
|
||||
@@ -674,16 +679,6 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
printTimings();
|
||||
await runRpcMode(runtime);
|
||||
} else if (appMode === "interactive") {
|
||||
if (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {
|
||||
const modelList = scopedModels
|
||||
.map((sm) => {
|
||||
const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
|
||||
return `${sm.model.id}${thinkingStr}`;
|
||||
})
|
||||
.join(", ");
|
||||
console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
|
||||
}
|
||||
|
||||
const interactiveMode = new InteractiveMode(runtime, {
|
||||
migratedProviders,
|
||||
modelFallbackMessage,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import chalk from "chalk";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
|
||||
import { migrateKeybindingsConfig } from "./core/keybindings.js";
|
||||
import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.ts";
|
||||
import { migrateKeybindingsConfig } from "./core/keybindings.ts";
|
||||
|
||||
const MIGRATION_GUIDE_URL =
|
||||
"https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Run modes for the coding agent.
|
||||
*/
|
||||
|
||||
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.js";
|
||||
export { type PrintModeOptions, runPrintMode } from "./print-mode.js";
|
||||
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.js";
|
||||
export { runRpcMode } from "./rpc/rpc-mode.js";
|
||||
export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types.js";
|
||||
export { InteractiveMode, type InteractiveModeOptions } from "./interactive/interactive-mode.ts";
|
||||
export { type PrintModeOptions, runPrintMode } from "./print-mode.ts";
|
||||
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.ts";
|
||||
export { runRpcMode } from "./rpc/rpc-mode.ts";
|
||||
export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { Component, TUI } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground
|
||||
const WIDTH = 31;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
||||
import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
|
||||
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
||||
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
*/
|
||||
|
||||
import { Container, Loader, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import {
|
||||
DEFAULT_MAX_BYTES,
|
||||
DEFAULT_MAX_LINES,
|
||||
type TruncationResult,
|
||||
truncateTail,
|
||||
} from "../../../core/tools/truncate.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint, keyText } from "./keybinding-hints.js";
|
||||
import { truncateToVisualLines } from "./visual-truncate.js";
|
||||
} from "../../../core/tools/truncate.ts";
|
||||
import { stripAnsi } from "../../../utils/ansi.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint, keyText } from "./keybinding-hints.ts";
|
||||
import { truncateToVisualLines } from "./visual-truncate.ts";
|
||||
|
||||
// Preview line limit when not expanded (matches tool execution behavior)
|
||||
const PREVIEW_LINES = 20;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import type { Theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import type { Theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
/** Loader wrapped with borders for extension UI */
|
||||
export class BorderedLoader extends Container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import type { BranchSummaryMessage } from "../../../core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
import type { BranchSummaryMessage } from "../../../core/messages.ts";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
|
||||
/**
|
||||
* Component that renders a branch summary message with collapsed/expanded state.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import type { CompactionSummaryMessage } from "../../../core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
import type { CompactionSummaryMessage } from "../../../core/messages.ts";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
|
||||
/**
|
||||
* Component that renders a compaction message with collapsed/expanded state.
|
||||
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { CONFIG_DIR_NAME } from "../../../config.js";
|
||||
import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.js";
|
||||
import type { PackageSource, SettingsManager } from "../../../core/settings-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { rawKeyHint } from "./keybinding-hints.js";
|
||||
import { CONFIG_DIR_NAME } from "../../../config.ts";
|
||||
import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.ts";
|
||||
import type { PackageSource, SettingsManager } from "../../../core/settings-manager.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { rawKeyHint } from "./keybinding-hints.ts";
|
||||
|
||||
type ResourceType = "extensions" | "skills" | "prompts" | "themes";
|
||||
|
||||
@@ -201,7 +201,7 @@ class ResourceList implements Component, Focusable {
|
||||
private filteredItems: FlatEntry[] = [];
|
||||
private selectedIndex = 0;
|
||||
private searchInput: Input;
|
||||
private maxVisible = 15;
|
||||
private maxVisible: number;
|
||||
private settingsManager: SettingsManager;
|
||||
private cwd: string;
|
||||
private agentDir: string;
|
||||
@@ -219,12 +219,21 @@ class ResourceList implements Component, Focusable {
|
||||
this.searchInput.focused = value;
|
||||
}
|
||||
|
||||
constructor(groups: ResourceGroup[], settingsManager: SettingsManager, cwd: string, agentDir: string) {
|
||||
constructor(
|
||||
groups: ResourceGroup[],
|
||||
settingsManager: SettingsManager,
|
||||
cwd: string,
|
||||
agentDir: string,
|
||||
terminalHeight?: number,
|
||||
) {
|
||||
this.groups = groups;
|
||||
this.settingsManager = settingsManager;
|
||||
this.cwd = cwd;
|
||||
this.agentDir = agentDir;
|
||||
this.searchInput = new Input();
|
||||
// 8 lines of chrome: top spacer + top border + spacer + header (2 lines) + spacer + bottom spacer + bottom border
|
||||
const chrome = 8;
|
||||
this.maxVisible = Math.max(5, (terminalHeight ?? 24) - chrome);
|
||||
this.buildFlatList();
|
||||
this.filteredItems = [...this.flatItems];
|
||||
}
|
||||
@@ -588,6 +597,7 @@ export class ConfigSelectorComponent extends Container implements Focusable {
|
||||
onClose: () => void,
|
||||
onExit: () => void,
|
||||
requestRender: () => void,
|
||||
terminalHeight?: number,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -601,7 +611,7 @@ export class ConfigSelectorComponent extends Container implements Focusable {
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Resource list
|
||||
this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir);
|
||||
this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir, terminalHeight);
|
||||
this.resourceList.onCancel = onClose;
|
||||
this.resourceList.onExit = onExit;
|
||||
this.resourceList.onToggle = () => requestRender();
|
||||
|
||||
@@ -7,13 +7,14 @@ import type { TUI } from "@earendil-works/pi-tui";
|
||||
export class CountdownTimer {
|
||||
private intervalId: ReturnType<typeof setInterval> | undefined;
|
||||
private remainingSeconds: number;
|
||||
private tui: TUI | undefined;
|
||||
private onTick: (seconds: number) => void;
|
||||
private onExpire: () => void;
|
||||
|
||||
constructor(
|
||||
timeoutMs: number,
|
||||
private tui: TUI | undefined,
|
||||
private onTick: (seconds: number) => void,
|
||||
private onExpire: () => void,
|
||||
) {
|
||||
constructor(timeoutMs: number, tui: TUI | undefined, onTick: (seconds: number) => void, onExpire: () => void) {
|
||||
this.tui = tui;
|
||||
this.onTick = onTick;
|
||||
this.onExpire = onExpire;
|
||||
this.remainingSeconds = Math.ceil(timeoutMs / 1000);
|
||||
this.onTick(this.remainingSeconds);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Editor, type EditorOptions, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
|
||||
import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.ts";
|
||||
|
||||
/**
|
||||
* Custom editor that handles app-level keybindings for coding-agent.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { TextContent } from "@earendil-works/pi-ai";
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import type { MessageRenderer } from "../../../core/extensions/types.js";
|
||||
import type { CustomMessage } from "../../../core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import type { MessageRenderer } from "../../../core/extensions/types.ts";
|
||||
import type { CustomMessage } from "../../../core/messages.ts";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
|
||||
/**
|
||||
* Component that renders a custom message entry from extensions.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Component, TUI } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
// 32x32 RGB image of dax, hex encoded (3 bytes per pixel)
|
||||
const DAX_HEX =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Diff from "diff";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
/**
|
||||
* Parse diff line to extract prefix, line number, and content.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Component } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as fs from "node:fs";
|
||||
import { Container, Image, Spacer, Text } from "@earendil-works/pi-tui";
|
||||
import { getBundledInteractiveAssetPath } from "../../../config.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { getBundledInteractiveAssetPath } from "../../../config.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
const BLOG_URL = "https://mariozechner.at/posts/2026-04-08-ive-sold-out/";
|
||||
const IMAGE_FILENAME = "clankolas.png";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Supports Ctrl+G for external editor.
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
Text,
|
||||
type TUI,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import { getEditorTheme, theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import type { KeybindingsManager } from "../../../core/keybindings.ts";
|
||||
import { getEditorTheme, theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
private editor: Editor;
|
||||
@@ -110,7 +110,7 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
this.editor.handleInput(keyData);
|
||||
}
|
||||
|
||||
private openExternalEditor(): void {
|
||||
private async openExternalEditor(): Promise<void> {
|
||||
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
||||
if (!editorCmd) {
|
||||
return;
|
||||
@@ -124,12 +124,21 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
this.tui.stop();
|
||||
|
||||
const [editor, ...editorArgs] = editorCmd.split(" ");
|
||||
const result = spawnSync(editor, [...editorArgs, tmpFile], {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`);
|
||||
|
||||
// Do not use spawnSync here. On Windows, synchronous child_process calls can keep
|
||||
// Node/libuv's console input read active after tui.stop() pauses stdin, racing
|
||||
// vim/nvim for the console input buffer until Ctrl+C cancels the pending read.
|
||||
const status = await new Promise<number | null>((resolve) => {
|
||||
const child = spawn(editor, [...editorArgs, tmpFile], {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
child.on("error", () => resolve(null));
|
||||
child.on("close", (code) => resolve(code));
|
||||
});
|
||||
|
||||
if (result.status === 0) {
|
||||
if (status === 0) {
|
||||
const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
|
||||
this.editor.setText(newContent);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { CountdownTimer } from "./countdown-timer.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { CountdownTimer } from "./countdown-timer.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
export interface ExtensionInputOptions {
|
||||
tui?: TUI;
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
*/
|
||||
|
||||
import { Container, getKeybindings, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { CountdownTimer } from "./countdown-timer.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint, rawKeyHint } from "./keybinding-hints.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { CountdownTimer } from "./countdown-timer.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint, rawKeyHint } from "./keybinding-hints.ts";
|
||||
|
||||
export interface ExtensionSelectorOptions {
|
||||
tui?: TUI;
|
||||
timeout?: number;
|
||||
onToggleToolsExpanded?: () => void;
|
||||
}
|
||||
|
||||
export class ExtensionSelectorComponent extends Container {
|
||||
@@ -23,6 +24,7 @@ export class ExtensionSelectorComponent extends Container {
|
||||
private titleText: Text;
|
||||
private baseTitle: string;
|
||||
private countdown: CountdownTimer | undefined;
|
||||
private onToggleToolsExpanded: (() => void) | undefined;
|
||||
|
||||
constructor(
|
||||
title: string,
|
||||
@@ -36,6 +38,7 @@ export class ExtensionSelectorComponent extends Container {
|
||||
this.options = options;
|
||||
this.onSelectCallback = onSelect;
|
||||
this.onCancelCallback = onCancel;
|
||||
this.onToggleToolsExpanded = opts?.onToggleToolsExpanded;
|
||||
this.baseTitle = title;
|
||||
|
||||
this.addChild(new DynamicBorder());
|
||||
@@ -87,7 +90,9 @@ export class ExtensionSelectorComponent extends Container {
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
|
||||
if (kb.matches(keyData, "app.tools.expand")) {
|
||||
this.onToggleToolsExpanded?.();
|
||||
} else if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.updateList();
|
||||
} else if (kb.matches(keyData, "tui.select.down") || keyData === "j") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
||||
import type { AgentSession } from "../../../core/agent-session.js";
|
||||
import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import type { AgentSession } from "../../../core/agent-session.ts";
|
||||
import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
/**
|
||||
* Sanitize text for display in a single-line status.
|
||||
@@ -16,7 +16,7 @@ function sanitizeStatusText(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format token counts (similar to web-ui)
|
||||
* Format token counts for compact footer display.
|
||||
*/
|
||||
function formatTokens(count: number): string {
|
||||
if (count < 1000) return count.toString();
|
||||
@@ -32,11 +32,13 @@ function formatTokens(count: number): string {
|
||||
*/
|
||||
export class FooterComponent implements Component {
|
||||
private autoCompactEnabled = true;
|
||||
private session: AgentSession;
|
||||
private footerData: ReadonlyFooterDataProvider;
|
||||
|
||||
constructor(
|
||||
private session: AgentSession,
|
||||
private footerData: ReadonlyFooterDataProvider,
|
||||
) {}
|
||||
constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {
|
||||
this.session = session;
|
||||
this.footerData = footerData;
|
||||
}
|
||||
|
||||
setSession(session: AgentSession): void {
|
||||
this.session = session;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
// UI Components for extensions
|
||||
export { ArminComponent } from "./armin.js";
|
||||
export { AssistantMessageComponent } from "./assistant-message.js";
|
||||
export { BashExecutionComponent } from "./bash-execution.js";
|
||||
export { BorderedLoader } from "./bordered-loader.js";
|
||||
export { BranchSummaryMessageComponent } from "./branch-summary-message.js";
|
||||
export { CompactionSummaryMessageComponent } from "./compaction-summary-message.js";
|
||||
export { CustomEditor } from "./custom-editor.js";
|
||||
export { CustomMessageComponent } from "./custom-message.js";
|
||||
export { DaxnutsComponent } from "./daxnuts.js";
|
||||
export { type RenderDiffOptions, renderDiff } from "./diff.js";
|
||||
export { DynamicBorder } from "./dynamic-border.js";
|
||||
export { ExtensionEditorComponent } from "./extension-editor.js";
|
||||
export { ExtensionInputComponent } from "./extension-input.js";
|
||||
export { ExtensionSelectorComponent } from "./extension-selector.js";
|
||||
export { FooterComponent } from "./footer.js";
|
||||
export { keyHint, keyText, rawKeyHint } from "./keybinding-hints.js";
|
||||
export { LoginDialogComponent } from "./login-dialog.js";
|
||||
export { ModelSelectorComponent } from "./model-selector.js";
|
||||
export { OAuthSelectorComponent } from "./oauth-selector.js";
|
||||
export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.js";
|
||||
export { SessionSelectorComponent } from "./session-selector.js";
|
||||
export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js";
|
||||
export { ShowImagesSelectorComponent } from "./show-images-selector.js";
|
||||
export { SkillInvocationMessageComponent } from "./skill-invocation-message.js";
|
||||
export { ThemeSelectorComponent } from "./theme-selector.js";
|
||||
export { ThinkingSelectorComponent } from "./thinking-selector.js";
|
||||
export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.js";
|
||||
export { TreeSelectorComponent } from "./tree-selector.js";
|
||||
export { UserMessageComponent } from "./user-message.js";
|
||||
export { UserMessageSelectorComponent } from "./user-message-selector.js";
|
||||
export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate.js";
|
||||
export { ArminComponent } from "./armin.ts";
|
||||
export { AssistantMessageComponent } from "./assistant-message.ts";
|
||||
export { BashExecutionComponent } from "./bash-execution.ts";
|
||||
export { BorderedLoader } from "./bordered-loader.ts";
|
||||
export { BranchSummaryMessageComponent } from "./branch-summary-message.ts";
|
||||
export { CompactionSummaryMessageComponent } from "./compaction-summary-message.ts";
|
||||
export { CustomEditor } from "./custom-editor.ts";
|
||||
export { CustomMessageComponent } from "./custom-message.ts";
|
||||
export { DaxnutsComponent } from "./daxnuts.ts";
|
||||
export { type RenderDiffOptions, renderDiff } from "./diff.ts";
|
||||
export { DynamicBorder } from "./dynamic-border.ts";
|
||||
export { ExtensionEditorComponent } from "./extension-editor.ts";
|
||||
export { ExtensionInputComponent } from "./extension-input.ts";
|
||||
export { ExtensionSelectorComponent } from "./extension-selector.ts";
|
||||
export { FooterComponent } from "./footer.ts";
|
||||
export { keyHint, keyText, rawKeyHint } from "./keybinding-hints.ts";
|
||||
export { LoginDialogComponent } from "./login-dialog.ts";
|
||||
export { ModelSelectorComponent } from "./model-selector.ts";
|
||||
export { OAuthSelectorComponent } from "./oauth-selector.ts";
|
||||
export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.ts";
|
||||
export { SessionSelectorComponent } from "./session-selector.ts";
|
||||
export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.ts";
|
||||
export { ShowImagesSelectorComponent } from "./show-images-selector.ts";
|
||||
export { SkillInvocationMessageComponent } from "./skill-invocation-message.ts";
|
||||
export { ThemeSelectorComponent } from "./theme-selector.ts";
|
||||
export { ThinkingSelectorComponent } from "./thinking-selector.ts";
|
||||
export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.ts";
|
||||
export { TreeSelectorComponent } from "./tree-selector.ts";
|
||||
export { UserMessageComponent } from "./user-message.ts";
|
||||
export { UserMessageSelectorComponent } from "./user-message-selector.ts";
|
||||
export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate.ts";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { getKeybindings, type Keybinding, type KeyId } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
export interface KeyTextFormatOptions {
|
||||
capitalize?: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
||||
import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import { exec } from "child_process";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
/**
|
||||
* Login dialog component - replaces editor during OAuth login flow
|
||||
@@ -15,6 +15,7 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
private abortController = new AbortController();
|
||||
private inputResolver?: (value: string) => void;
|
||||
private inputRejecter?: (error: Error) => void;
|
||||
private onComplete: (success: boolean, message?: string) => void;
|
||||
|
||||
// Focusable implementation - propagate to input for IME cursor positioning
|
||||
private _focused = false;
|
||||
@@ -29,12 +30,13 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
constructor(
|
||||
tui: TUI,
|
||||
providerId: string,
|
||||
private onComplete: (success: boolean, message?: string) => void,
|
||||
onComplete: (success: boolean, message?: string) => void,
|
||||
providerNameOverride?: string,
|
||||
titleOverride?: string,
|
||||
) {
|
||||
super();
|
||||
this.tui = tui;
|
||||
this.onComplete = onComplete;
|
||||
|
||||
const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
|
||||
const providerName = providerNameOverride || providerInfo?.name || providerId;
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
Text,
|
||||
type TUI,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { ModelRegistry } from "../../../core/model-registry.js";
|
||||
import type { SettingsManager } from "../../../core/settings-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
import type { ModelRegistry } from "../../../core/model-registry.ts";
|
||||
import type { SettingsManager } from "../../../core/settings-manager.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
interface ModelItem {
|
||||
provider: string;
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
Spacer,
|
||||
TruncatedText,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { AuthStatus, AuthStorage } from "../../../core/auth-storage.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import type { AuthStatus, AuthStorage } from "../../../core/auth-storage.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
export type AuthSelectorProvider = {
|
||||
id: string;
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
Spacer,
|
||||
Text,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
|
||||
// EnabledIds: null = all enabled (no filter), string[] = explicit ordered list
|
||||
type EnabledIds = string[] | null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fuzzyMatch } from "@earendil-works/pi-tui";
|
||||
import type { SessionInfo } from "../../../core/session-manager.js";
|
||||
import type { SessionInfo } from "../../../core/session-manager.ts";
|
||||
|
||||
export type SortMode = "threaded" | "recent" | "relevance";
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js";
|
||||
import { canonicalizePath as _canonicalizePath } from "../../../utils/paths.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint, keyText } from "./keybinding-hints.js";
|
||||
import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.js";
|
||||
import { KeybindingsManager } from "../../../core/keybindings.ts";
|
||||
import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.ts";
|
||||
import { canonicalizePath as _canonicalizePath } from "../../../utils/paths.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint, keyText } from "./keybinding-hints.ts";
|
||||
import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.ts";
|
||||
|
||||
type SessionScope = "current" | "all";
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
Spacer,
|
||||
Text,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { WarningSettings } from "../../../core/settings-manager.js";
|
||||
import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyDisplayText } from "./keybinding-hints.js";
|
||||
import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts";
|
||||
import type { WarningSettings } from "../../../core/settings-manager.ts";
|
||||
import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyDisplayText } from "./keybinding-hints.ts";
|
||||
|
||||
const SETTINGS_SUBMENU_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
||||
minPrimaryColumnWidth: 12,
|
||||
@@ -40,6 +41,7 @@ export interface SettingsConfig {
|
||||
steeringMode: "all" | "one-at-a-time";
|
||||
followUpMode: "all" | "one-at-a-time";
|
||||
transport: Transport;
|
||||
httpIdleTimeoutMs: number;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
availableThinkingLevels: ThinkingLevel[];
|
||||
currentTheme: string;
|
||||
@@ -68,6 +70,7 @@ export interface SettingsCallbacks {
|
||||
onSteeringModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||
onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||
onTransportChange: (transport: Transport) => void;
|
||||
onHttpIdleTimeoutMsChange: (timeoutMs: number) => void;
|
||||
onThinkingLevelChange: (level: ThinkingLevel) => void;
|
||||
onThemeChange: (theme: string) => void;
|
||||
onThemePreview?: (theme: string) => void;
|
||||
@@ -238,6 +241,14 @@ export class SettingsSelectorComponent extends Container {
|
||||
currentValue: config.transport,
|
||||
values: ["sse", "websocket", "websocket-cached", "auto"],
|
||||
},
|
||||
{
|
||||
id: "http-idle-timeout",
|
||||
label: "HTTP idle timeout",
|
||||
description:
|
||||
"Maximum idle gap while waiting for HTTP headers or body chunks. Disable for local models that pause longer than five minutes.",
|
||||
currentValue: formatHttpIdleTimeoutMs(config.httpIdleTimeoutMs),
|
||||
values: HTTP_IDLE_TIMEOUT_CHOICES.map((choice) => choice.label),
|
||||
},
|
||||
{
|
||||
id: "hide-thinking",
|
||||
label: "Hide thinking",
|
||||
@@ -482,6 +493,13 @@ export class SettingsSelectorComponent extends Container {
|
||||
case "transport":
|
||||
callbacks.onTransportChange(newValue as Transport);
|
||||
break;
|
||||
case "http-idle-timeout": {
|
||||
const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.label === newValue);
|
||||
if (choice) {
|
||||
callbacks.onHttpIdleTimeoutMsChange(choice.timeoutMs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hide-thinking":
|
||||
callbacks.onHideThinkingBlockChange(newValue === "true");
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { getSelectListTheme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
const SHOW_IMAGES_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
||||
minPrimaryColumnWidth: 12,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Text } from "@earendil-works/pi-tui";
|
||||
import type { ParsedSkillBlock } from "../../../core/agent-session.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
import type { ParsedSkillBlock } from "../../../core/agent-session.ts";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
|
||||
/**
|
||||
* Component that renders a skill invocation message with collapsed/expanded state.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui";
|
||||
import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { getAvailableThemes, getSelectListTheme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
const THEME_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
||||
minPrimaryColumnWidth: 12,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { Container, type SelectItem, SelectList, type SelectListLayoutOptions } from "@earendil-works/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { getSelectListTheme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
const THINKING_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {
|
||||
minPrimaryColumnWidth: 12,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions/types.js";
|
||||
import { createAllToolDefinitions, type ToolName } from "../../../core/tools/index.js";
|
||||
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
|
||||
import { convertToPng } from "../../../utils/image-convert.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions/types.ts";
|
||||
import { createAllToolDefinitions, type ToolName } from "../../../core/tools/index.ts";
|
||||
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.ts";
|
||||
import { convertToPng } from "../../../utils/image-convert.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
export interface ToolExecutionOptions {
|
||||
showImages?: boolean;
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
TruncatedText,
|
||||
truncateToWidth,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import type { SessionTreeNode } from "../../../core/session-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { keyHint, keyText } from "./keybinding-hints.js";
|
||||
import type { SessionTreeNode } from "../../../core/session-manager.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint, keyText } from "./keybinding-hints.ts";
|
||||
|
||||
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
|
||||
interface GutterInfo {
|
||||
@@ -1056,7 +1056,11 @@ class TreeList implements Component {
|
||||
|
||||
/** Component that displays the current search query */
|
||||
class SearchLine implements Component {
|
||||
constructor(private treeList: TreeList) {}
|
||||
private treeList: TreeList;
|
||||
|
||||
constructor(treeList: TreeList) {
|
||||
this.treeList = treeList;
|
||||
}
|
||||
|
||||
invalidate(): void {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Component, Container, getKeybindings, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
|
||||
interface UserMessageItem {
|
||||
id: string; // Entry ID in the session
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Container, Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.ts";
|
||||
|
||||
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
||||
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
||||
|
||||
@@ -57,9 +57,9 @@ import {
|
||||
getDocsPath,
|
||||
getShareViewerUrl,
|
||||
VERSION,
|
||||
} from "../../config.js";
|
||||
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
|
||||
import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
|
||||
} from "../../config.ts";
|
||||
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.ts";
|
||||
import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.ts";
|
||||
import type {
|
||||
AutocompleteProviderFactory,
|
||||
EditorFactory,
|
||||
@@ -69,57 +69,58 @@ import type {
|
||||
ExtensionUIContext,
|
||||
ExtensionUIDialogOptions,
|
||||
ExtensionWidgetOptions,
|
||||
} from "../../core/extensions/index.js";
|
||||
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js";
|
||||
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.js";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
||||
import { DefaultPackageManager } from "../../core/package-manager.js";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
||||
import type { SourceInfo } from "../../core/source-info.js";
|
||||
import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
|
||||
import type { TruncationResult } from "../../core/tools/truncate.js";
|
||||
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
||||
import { copyToClipboard } from "../../utils/clipboard.js";
|
||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
||||
import { parseGitUrl } from "../../utils/git.js";
|
||||
import { getCwdRelativePath } from "../../utils/paths.js";
|
||||
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
||||
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import { checkForNewPiVersion } from "../../utils/version-check.js";
|
||||
import { ArminComponent } from "./components/armin.js";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.js";
|
||||
import { BorderedLoader } from "./components/bordered-loader.js";
|
||||
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
|
||||
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
|
||||
import { CountdownTimer } from "./components/countdown-timer.js";
|
||||
import { CustomEditor } from "./components/custom-editor.js";
|
||||
import { CustomMessageComponent } from "./components/custom-message.js";
|
||||
import { DaxnutsComponent } from "./components/daxnuts.js";
|
||||
import { DynamicBorder } from "./components/dynamic-border.js";
|
||||
import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js";
|
||||
import { ExtensionEditorComponent } from "./components/extension-editor.js";
|
||||
import { ExtensionInputComponent } from "./components/extension-input.js";
|
||||
import { ExtensionSelectorComponent } from "./components/extension-selector.js";
|
||||
import { FooterComponent } from "./components/footer.js";
|
||||
import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
||||
import { LoginDialogComponent } from "./components/login-dialog.js";
|
||||
import { ModelSelectorComponent } from "./components/model-selector.js";
|
||||
import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.js";
|
||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
||||
import { SessionSelectorComponent } from "./components/session-selector.js";
|
||||
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
||||
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
|
||||
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
||||
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
||||
import { UserMessageComponent } from "./components/user-message.js";
|
||||
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
||||
} from "../../core/extensions/index.ts";
|
||||
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.ts";
|
||||
import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.ts";
|
||||
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.ts";
|
||||
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts";
|
||||
import { DefaultPackageManager } from "../../core/package-manager.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
|
||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
|
||||
import { type SessionContext, SessionManager } from "../../core/session-manager.ts";
|
||||
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts";
|
||||
import type { SourceInfo } from "../../core/source-info.ts";
|
||||
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
|
||||
import type { TruncationResult } from "../../core/tools/truncate.ts";
|
||||
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.ts";
|
||||
import { copyToClipboard } from "../../utils/clipboard.ts";
|
||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
|
||||
import { parseGitUrl } from "../../utils/git.ts";
|
||||
import { getCwdRelativePath } from "../../utils/paths.ts";
|
||||
import { getPiUserAgent } from "../../utils/pi-user-agent.ts";
|
||||
import { killTrackedDetachedChildren } from "../../utils/shell.ts";
|
||||
import { ensureTool } from "../../utils/tools-manager.ts";
|
||||
import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.ts";
|
||||
import { ArminComponent } from "./components/armin.ts";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.ts";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.ts";
|
||||
import { BorderedLoader } from "./components/bordered-loader.ts";
|
||||
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.ts";
|
||||
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.ts";
|
||||
import { CountdownTimer } from "./components/countdown-timer.ts";
|
||||
import { CustomEditor } from "./components/custom-editor.ts";
|
||||
import { CustomMessageComponent } from "./components/custom-message.ts";
|
||||
import { DaxnutsComponent } from "./components/daxnuts.ts";
|
||||
import { DynamicBorder } from "./components/dynamic-border.ts";
|
||||
import { EarendilAnnouncementComponent } from "./components/earendil-announcement.ts";
|
||||
import { ExtensionEditorComponent } from "./components/extension-editor.ts";
|
||||
import { ExtensionInputComponent } from "./components/extension-input.ts";
|
||||
import { ExtensionSelectorComponent } from "./components/extension-selector.ts";
|
||||
import { FooterComponent } from "./components/footer.ts";
|
||||
import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.ts";
|
||||
import { LoginDialogComponent } from "./components/login-dialog.ts";
|
||||
import { ModelSelectorComponent } from "./components/model-selector.ts";
|
||||
import { type AuthSelectorProvider, OAuthSelectorComponent } from "./components/oauth-selector.ts";
|
||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts";
|
||||
import { SessionSelectorComponent } from "./components/session-selector.ts";
|
||||
import { SettingsSelectorComponent } from "./components/settings-selector.ts";
|
||||
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.ts";
|
||||
import { ToolExecutionComponent } from "./components/tool-execution.ts";
|
||||
import { TreeSelectorComponent } from "./components/tree-selector.ts";
|
||||
import { UserMessageComponent } from "./components/user-message.ts";
|
||||
import { UserMessageSelectorComponent } from "./components/user-message-selector.ts";
|
||||
import {
|
||||
getAvailableThemes,
|
||||
getAvailableThemesWithPaths,
|
||||
@@ -135,7 +136,7 @@ import {
|
||||
Theme,
|
||||
type ThemeColor,
|
||||
theme,
|
||||
} from "./theme/theme.js";
|
||||
} from "./theme/theme.ts";
|
||||
|
||||
/** Interface for components that can be expanded/collapsed */
|
||||
interface Expandable {
|
||||
@@ -147,14 +148,19 @@ function isExpandable(obj: unknown): obj is Expandable {
|
||||
}
|
||||
|
||||
class ExpandableText extends Text implements Expandable {
|
||||
private readonly getCollapsedText: () => string;
|
||||
private readonly getExpandedText: () => string;
|
||||
|
||||
constructor(
|
||||
private readonly getCollapsedText: () => string,
|
||||
private readonly getExpandedText: () => string,
|
||||
getCollapsedText: () => string,
|
||||
getExpandedText: () => string,
|
||||
expanded = false,
|
||||
paddingX = 0,
|
||||
paddingY = 0,
|
||||
) {
|
||||
super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
|
||||
this.getCollapsedText = getCollapsedText;
|
||||
this.getExpandedText = getExpandedText;
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean): void {
|
||||
@@ -334,6 +340,8 @@ export class InteractiveMode {
|
||||
// Custom header from extension (undefined = use built-in header)
|
||||
private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
|
||||
|
||||
private options: InteractiveModeOptions;
|
||||
|
||||
// Convenience accessors
|
||||
private get session(): AgentSession {
|
||||
return this.runtimeHost.session;
|
||||
@@ -348,11 +356,9 @@ export class InteractiveMode {
|
||||
return this.session.settingsManager;
|
||||
}
|
||||
|
||||
constructor(
|
||||
runtimeHost: AgentSessionRuntime,
|
||||
private options: InteractiveModeOptions = {},
|
||||
) {
|
||||
constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) {
|
||||
this.runtimeHost = runtimeHost;
|
||||
this.options = options;
|
||||
this.runtimeHost.setBeforeSessionInvalidate(() => {
|
||||
this.resetExtensionUI();
|
||||
});
|
||||
@@ -572,6 +578,21 @@ export class InteractiveMode {
|
||||
const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
|
||||
this.fdPath = fdPath;
|
||||
|
||||
if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
|
||||
const modelList = this.session.scopedModels
|
||||
.map((sm) => {
|
||||
const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
|
||||
return `${sm.model.id}${thinkingStr}`;
|
||||
})
|
||||
.join(", ");
|
||||
const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
|
||||
const cycleHint =
|
||||
cycleKeys.length > 0
|
||||
? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
|
||||
: "";
|
||||
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
||||
}
|
||||
|
||||
// Add header container as first child
|
||||
this.ui.addChild(this.headerContainer);
|
||||
|
||||
@@ -696,9 +717,9 @@ export class InteractiveMode {
|
||||
await this.init();
|
||||
|
||||
// Start version check asynchronously
|
||||
checkForNewPiVersion(this.version).then((newVersion) => {
|
||||
if (newVersion) {
|
||||
this.showNewVersionNotification(newVersion);
|
||||
checkForNewPiVersion(this.version).then((newRelease) => {
|
||||
if (newRelease) {
|
||||
this.showNewVersionNotification(newRelease);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1463,6 +1484,9 @@ export class InteractiveMode {
|
||||
const uiContext = this.createExtensionUIContext();
|
||||
await this.session.bindExtensions({
|
||||
uiContext,
|
||||
abortHandler: () => {
|
||||
this.restoreQueuedMessagesToEditor({ abort: true });
|
||||
},
|
||||
commandContextActions: {
|
||||
waitForIdle: () => this.session.agent.waitForIdle(),
|
||||
newSession: async (options) => {
|
||||
@@ -1543,6 +1567,7 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
private applyRuntimeSettings(): void {
|
||||
configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
|
||||
this.footer.setSession(this.session);
|
||||
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
||||
@@ -1612,7 +1637,9 @@ export class InteractiveMode {
|
||||
model: this.session.model,
|
||||
isIdle: () => !this.session.isStreaming,
|
||||
signal: this.session.agent.signal,
|
||||
abort: () => this.session.abort(),
|
||||
abort: () => {
|
||||
this.restoreQueuedMessagesToEditor({ abort: true });
|
||||
},
|
||||
hasPendingMessages: () => this.session.pendingMessageCount > 0,
|
||||
shutdown: () => {
|
||||
this.shutdownRequested = true;
|
||||
@@ -2024,7 +2051,7 @@ export class InteractiveMode {
|
||||
this.hideExtensionSelector();
|
||||
resolve(undefined);
|
||||
},
|
||||
{ tui: this.ui, timeout: opts?.timeout },
|
||||
{ tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() },
|
||||
);
|
||||
|
||||
this.editorContainer.clear();
|
||||
@@ -3244,6 +3271,36 @@ export class InteractiveMode {
|
||||
process.exit(129);
|
||||
}
|
||||
|
||||
/**
|
||||
* Last-resort handler for uncaught exceptions. The TUI puts stdin into raw
|
||||
* mode and hides the cursor; without this handler, an uncaught throw from
|
||||
* anywhere (e.g. an extension's async `ChildProcess.on("exit")` callback)
|
||||
* tears down the process while leaving the terminal in raw mode with no
|
||||
* cursor, requiring `stty sane && reset` to recover.
|
||||
*
|
||||
* Unlike emergencyTerminalExit, the terminal is still alive here, so we
|
||||
* call ui.stop() to restore cooked mode, the cursor, and disable bracketed
|
||||
* paste / Kitty / modifyOtherKeys sequences.
|
||||
*/
|
||||
private uncaughtCrash(error: Error): never {
|
||||
if (this.isShuttingDown) {
|
||||
process.exit(1);
|
||||
}
|
||||
this.isShuttingDown = true;
|
||||
try {
|
||||
this.unregisterSignalHandlers();
|
||||
} catch {}
|
||||
try {
|
||||
killTrackedDetachedChildren();
|
||||
} catch {}
|
||||
try {
|
||||
this.ui.stop();
|
||||
} catch {}
|
||||
console.error("pi exiting due to uncaughtException:");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shutdown was requested and perform shutdown if so.
|
||||
*/
|
||||
@@ -3282,6 +3339,13 @@ export class InteractiveMode {
|
||||
process.stderr.on("error", terminalErrorHandler);
|
||||
this.signalCleanupHandlers.push(() => process.stdout.off("error", terminalErrorHandler));
|
||||
this.signalCleanupHandlers.push(() => process.stderr.off("error", terminalErrorHandler));
|
||||
|
||||
// Restore the terminal before the process dies on any uncaught throw.
|
||||
// Without this, an unhandled exception from extension code (or anywhere
|
||||
// in pi) leaves the terminal in raw mode with no cursor.
|
||||
const uncaughtExceptionHandler = (error: Error) => this.uncaughtCrash(error);
|
||||
process.prependListener("uncaughtException", uncaughtExceptionHandler);
|
||||
this.signalCleanupHandlers.push(() => process.off("uncaughtException", uncaughtExceptionHandler));
|
||||
}
|
||||
|
||||
private unregisterSignalHandlers(): void {
|
||||
@@ -3445,7 +3509,7 @@ export class InteractiveMode {
|
||||
this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
|
||||
}
|
||||
|
||||
private openExternalEditor(): void {
|
||||
private async openExternalEditor(): Promise<void> {
|
||||
// Determine editor (respect $VISUAL, then $EDITOR)
|
||||
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
||||
if (!editorCmd) {
|
||||
@@ -3466,14 +3530,22 @@ export class InteractiveMode {
|
||||
// Split by space to support editor arguments (e.g., "code --wait")
|
||||
const [editor, ...editorArgs] = editorCmd.split(" ");
|
||||
|
||||
// Spawn editor synchronously with inherited stdio for interactive editing
|
||||
const result = spawnSync(editor, [...editorArgs, tmpFile], {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`);
|
||||
|
||||
// Do not use spawnSync here. On Windows, synchronous child_process calls can keep
|
||||
// Node/libuv's console input read active after ui.stop() pauses stdin, racing
|
||||
// vim/nvim for the console input buffer until Ctrl+C cancels the pending read.
|
||||
const status = await new Promise<number | null>((resolve) => {
|
||||
const child = spawn(editor, [...editorArgs, tmpFile], {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
child.on("error", () => resolve(null));
|
||||
child.on("close", (code) => resolve(code));
|
||||
});
|
||||
|
||||
// On successful exit (status 0), replace editor content
|
||||
if (result.status === 0) {
|
||||
if (status === 0) {
|
||||
const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
|
||||
this.editor.setText(newContent);
|
||||
}
|
||||
@@ -3505,6 +3577,7 @@ export class InteractiveMode {
|
||||
showError(errorMessage: string): void {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
@@ -3514,24 +3587,31 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showNewVersionNotification(newVersion: string): void {
|
||||
showNewVersionNotification(release: LatestPiRelease): void {
|
||||
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
|
||||
const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md";
|
||||
const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
|
||||
const changelogUrl = "https://pi.dev/changelog";
|
||||
const changelogLink = getCapabilities().hyperlinks
|
||||
? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
|
||||
: theme.fg("accent", changelogUrl);
|
||||
const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
|
||||
const note = release.note?.trim();
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(
|
||||
`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0),
|
||||
);
|
||||
if (note) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
|
||||
color: (text) => theme.fg("muted", text),
|
||||
}),
|
||||
);
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
this.chatContainer.addChild(new Text(changelogLine, 1, 0));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
@@ -3768,6 +3848,7 @@ export class InteractiveMode {
|
||||
steeringMode: this.session.steeringMode,
|
||||
followUpMode: this.session.followUpMode,
|
||||
transport: this.settingsManager.getTransport(),
|
||||
httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
|
||||
thinkingLevel: this.session.thinkingLevel,
|
||||
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
||||
currentTheme: this.settingsManager.getTheme() || "dark",
|
||||
@@ -3826,6 +3907,11 @@ export class InteractiveMode {
|
||||
this.settingsManager.setTransport(transport);
|
||||
this.session.agent.transport = transport;
|
||||
},
|
||||
onHttpIdleTimeoutMsChange: (timeoutMs) => {
|
||||
this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
|
||||
configureHttpDispatcher(timeoutMs);
|
||||
this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`);
|
||||
},
|
||||
onThinkingLevelChange: (level) => {
|
||||
this.session.setThinkingLevel(level);
|
||||
this.footer.invalidate();
|
||||
@@ -4813,6 +4899,7 @@ export class InteractiveMode {
|
||||
|
||||
try {
|
||||
await this.session.reload();
|
||||
configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
|
||||
this.keybindings.reload();
|
||||
const activeHeader = this.customHeader ?? this.builtInHeader;
|
||||
if (isExpandable(activeHeader)) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"green": "#b5bd68",
|
||||
"red": "#cc6666",
|
||||
"yellow": "#ffff00",
|
||||
"text": "#d4d4d4",
|
||||
"gray": "#808080",
|
||||
"dimGray": "#666666",
|
||||
"darkGray": "#505050",
|
||||
@@ -28,19 +29,19 @@
|
||||
"warning": "yellow",
|
||||
"muted": "gray",
|
||||
"dim": "dimGray",
|
||||
"text": "",
|
||||
"text": "text",
|
||||
"thinkingText": "gray",
|
||||
|
||||
"selectedBg": "selectedBg",
|
||||
"userMessageBg": "userMsgBg",
|
||||
"userMessageText": "",
|
||||
"userMessageText": "text",
|
||||
"customMessageBg": "customMsgBg",
|
||||
"customMessageText": "",
|
||||
"customMessageText": "text",
|
||||
"customMessageLabel": "#9575cd",
|
||||
"toolPendingBg": "toolPendingBg",
|
||||
"toolSuccessBg": "toolSuccessBg",
|
||||
"toolErrorBg": "toolErrorBg",
|
||||
"toolTitle": "",
|
||||
"toolTitle": "text",
|
||||
"toolOutput": "gray",
|
||||
|
||||
"mdHeading": "#f0c674",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user