Merge branch 'main' into async-file-tools

This commit is contained in:
Armin Ronacher
2026-05-20 17:14:01 +02:00
295 changed files with 3915 additions and 1034 deletions
+22
View File
@@ -2,19 +2,41 @@
## [Unreleased]
### New Features
- **Hardened npm install and release path** - Pi now ships the CLI with a generated shrinkwrap for transitive dependencies, blocks accidental lockfile changes, verifies dependency pinning and lifecycle-script allowlists in checks, disables lifecycle scripts for self-update and local release installs where supported, and smoke-tests isolated npm and Bun installs before release. See [Supply-chain hardening](../../README.md#supply-chain-hardening).
### Added
- Added interactive update notes after `pi update` runs, so users can see the installed version's changelog before continuing ([#4724](https://github.com/earendil-works/pi-mono/pull/4724) by [@mitsuhiko](https://github.com/mitsuhiko)).
- Exported image resize utilities from the package root for SDK consumers ([#4775](https://github.com/earendil-works/pi-mono/pull/4775) by [@xl0](https://github.com/xl0)).
### Changed
- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks.
- Removed web UI workspace references from the CLI package and dropped the package-level development watch script.
- Published npm installs now include an `npm-shrinkwrap.json` to lock transitive dependencies for the CLI package.
- Improved terminal theme detection for light/dark and truecolor handling.
- Changed self-update package-manager commands to disable lifecycle scripts during reinstall.
### Fixed
- Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)).
- Fixed extension `ctx.abort()` during tool-call preflight to stop later confirmations and restore queued interactive input like Escape ([#4276](https://github.com/earendil-works/pi/issues/4276)).
- Fixed AgentSession retry, compaction, and event settlement to use the awaited agent lifecycle instead of a separate event queue, and added `willRetry` to `agent_end` session events.
- Fixed forked session runtime state to keep the active session id aligned with the fork target ([#4799](https://github.com/earendil-works/pi-mono/pull/4799) by [@Perlence](https://github.com/Perlence)).
- Fixed the subagent extension's parallel mode to return useful per-task output and failed-task diagnostics to the parent model instead of 100-character previews ([#4710](https://github.com/earendil-works/pi/issues/4710)).
- Fixed Windows local bash execution to hide helper console windows when launched from background SDK processes ([#4699](https://github.com/earendil-works/pi/issues/4699)).
- Fixed managed npm extension folders to set cloud-sync ignore metadata where supported ([#4763](https://github.com/earendil-works/pi/issues/4763)).
- Fixed HTTP idle timeout configuration so long-running provider streams can avoid premature idle disconnects ([#4759](https://github.com/earendil-works/pi-mono/pull/4759) by [@mitsuhiko](https://github.com/mitsuhiko)).
- Fixed default system prompt boundaries to use explicit XML tags for clearer file separation ([#4709](https://github.com/earendil-works/pi-mono/pull/4709) by [@herrnel](https://github.com/herrnel)).
- Fixed HTML share/export sidebar clicks for shared tool entries to scroll to the rendered tool call ([#4664](https://github.com/earendil-works/pi-mono/pull/4664) by [@yzhg1983](https://github.com/yzhg1983)).
- Fixed theme palettes to set explicit text colors and avoid terminal-default color drift.
- Fixed truecolor detection to align terminal image rendering and interactive theme decisions.
- Fixed loader indicator startup inherited from `@earendil-works/pi-tui` so initialization cannot run before frames are available.
- Fixed OpenAI-compatible default output token requests inherited from `@earendil-works/pi-ai` to avoid reserving impossible context windows on servers such as vLLM ([#4675](https://github.com/earendil-works/pi/issues/4675)).
- Fixed OpenAI prompt cache keys inherited from `@earendil-works/pi-ai` to stay within the 64-character provider limit ([#4720](https://github.com/earendil-works/pi/issues/4720)).
- Fixed Windows npm-family package commands for fnm-managed Node.js installs that expose both extensionless Unix scripts and `.cmd` shims ([#4793](https://github.com/earendil-works/pi/issues/4793)).
## [0.75.3] - 2026-05-18
+5 -3
View File
@@ -68,13 +68,15 @@ I regularly publish my own `pi-mono` work sessions here:
## Quick Start
```bash
curl -fsSL https://pi.dev/install.sh | sh
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```
Or with npm:
`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
Installer alternative:
```bash
npm install -g @earendil-works/pi-coding-agent
curl -fsSL https://pi.dev/install.sh | sh
```
Authenticate with an API key:
+9 -7
View File
@@ -4,18 +4,20 @@ Pi is a minimal terminal coding harness. It is designed to stay small at the cor
## Quick start
On linux or mac you can install Pi with curl:
Install Pi with npm:
```bash
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```
`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
On Linux or macOS, you can also use the installer:
```bash
curl -fsSL https://pi.dev/install.sh | sh
```
Or alternatively with npm:
```bash
npm install -g @earendil-works/pi-coding-agent
```
To uninstall pi itself, use npm for curl and npm installs:
```bash
+3 -1
View File
@@ -7,9 +7,11 @@ This page gets you from install to a useful first pi session.
Pi is distributed as an npm package:
```bash
npm install -g @earendil-works/pi-coding-agent
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
```
`--ignore-scripts` disables dependency lifecycle scripts during install. Pi does not require install scripts for normal npm installs.
### Uninstall
Use the package manager that installed pi. The curl installer uses npm globally, so curl and npm installs are removed with npm:
+1 -1
View File
@@ -17,7 +17,7 @@ pkg update && pkg upgrade
pkg install nodejs termux-api git
# Install pi
npm install -g @earendil-works/pi-coding-agent
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
# Create config directory
mkdir -p ~/.pi/agent
@@ -14,6 +14,6 @@
]
},
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0"
"@anthropic-ai/sdk": "0.52.0"
}
}
@@ -12,7 +12,7 @@ import { type Api, type Context, type Model, registerApiProvider, streamSimple }
import { readFileSync } from "fs";
import { getAgentDir } from "packages/coding-agent/src/config.js";
import { join } from "path";
import { MODELS, streamGitLabDuo } from "./index.js";
import { MODELS, streamGitLabDuo } from "./index.ts";
const MODEL_MAP = new Map(MODELS.map((m) => [m.id, m]));
@@ -7,8 +7,8 @@
import type { Component } from "@earendil-works/pi-tui";
import { isKeyRelease, type TUI } from "@earendil-works/pi-tui";
import type { DoomEngine } from "./doom-engine.js";
import { DoomKeys, mapKeyToDoom } from "./doom-keys.js";
import type { DoomEngine } from "./doom-engine.ts";
import { DoomKeys, mapKeyToDoom } from "./doom-keys.ts";
function renderHalfBlock(
rgba: Uint8Array,
@@ -10,9 +10,9 @@
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { DoomOverlayComponent } from "./doom-component.js";
import { DoomEngine } from "./doom-engine.js";
import { ensureWadFile } from "./wad-finder.js";
import { DoomOverlayComponent } from "./doom-component.ts";
import { DoomEngine } from "./doom-engine.ts";
import { ensureWadFile } from "./wad-finder.ts";
// Persistent engine instance - survives between invocations
let activeEngine: DoomEngine | null = null;
@@ -16,7 +16,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { Key } from "@earendil-works/pi-tui";
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.ts";
// Tools
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
@@ -14,6 +14,6 @@
]
},
"dependencies": {
"@anthropic-ai/sandbox-runtime": "^0.0.26"
"@anthropic-ai/sandbox-runtime": "0.0.26"
}
}
@@ -22,7 +22,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
import { Type } from "typebox";
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
const MAX_PARALLEL_TASKS = 8;
const MAX_CONCURRENCY = 4;
@@ -14,9 +14,9 @@
]
},
"dependencies": {
"ms": "^2.1.3"
"ms": "2.1.3"
},
"devDependencies": {
"@types/ms": "^2.1.0"
"@types/ms": "2.1.0"
}
}
File diff suppressed because it is too large Load Diff
+28 -26
View File
@@ -25,7 +25,8 @@
"dist",
"docs",
"examples",
"CHANGELOG.md"
"CHANGELOG.md",
"npm-shrinkwrap.json"
],
"scripts": {
"clean": "shx rm -rf dist",
@@ -34,26 +35,27 @@
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
"test": "vitest --run",
"prepublishOnly": "npm run clean && npm run build"
"shrinkwrap": "node ../../scripts/generate-coding-agent-shrinkwrap.mjs",
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
},
"dependencies": {
"@earendil-works/pi-agent-core": "^0.75.3",
"@earendil-works/pi-ai": "^0.75.3",
"@earendil-works/pi-tui": "^0.75.3",
"@silvia-odwyer/photon-node": "^0.3.4",
"chalk": "^5.5.0",
"cross-spawn": "^7.0.6",
"diff": "^8.0.2",
"glob": "^13.0.1",
"highlight.js": "^10.7.3",
"hosted-git-info": "^9.0.2",
"ignore": "^7.0.5",
"jiti": "^2.7.0",
"minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2",
"typebox": "^1.1.24",
"undici": "^8.3.0",
"yaml": "^2.8.2"
"@silvia-odwyer/photon-node": "0.3.4",
"chalk": "5.6.2",
"cross-spawn": "7.0.6",
"diff": "8.0.4",
"glob": "13.0.6",
"highlight.js": "10.7.3",
"hosted-git-info": "9.0.3",
"ignore": "7.0.5",
"jiti": "2.7.0",
"minimatch": "10.2.5",
"proper-lockfile": "4.1.2",
"typebox": "1.1.38",
"undici": "8.3.0",
"yaml": "2.9.0"
},
"overrides": {
"rimraf": "6.1.2",
@@ -62,18 +64,18 @@
}
},
"optionalDependencies": {
"@mariozechner/clipboard": "^0.3.6"
"@mariozechner/clipboard": "0.3.6"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/diff": "^7.0.2",
"@types/hosted-git-info": "^3.0.5",
"@types/ms": "^2.1.0",
"@types/node": "^24.3.0",
"@types/proper-lockfile": "^4.1.4",
"shx": "^0.4.0",
"typescript": "^5.7.3",
"vitest": "^3.2.4"
"@types/cross-spawn": "6.0.6",
"@types/diff": "7.0.2",
"@types/hosted-git-info": "3.0.5",
"@types/ms": "2.1.0",
"@types/node": "24.12.4",
"@types/proper-lockfile": "4.1.4",
"shx": "0.4.0",
"typescript": "5.9.3",
"vitest": "3.2.4"
},
"keywords": [
"coding-agent",
+4 -11
View File
@@ -5,23 +5,16 @@
*
* Test with: npx tsx src/cli-new.ts [args...]
*/
import * as undici from "undici";
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.
undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({ allowH2: false, bodyTimeout: 0, headersTimeout: 0 }));
// 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?.();
// 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));
+10 -4
View File
@@ -110,21 +110,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]),
@@ -133,7 +133,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
@@ -292,12 +292,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({
@@ -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?.();
}
@@ -4,6 +4,7 @@ import { homedir } from "os";
import { dirname, join } from "path";
import lockfile from "proper-lockfile";
import { CONFIG_DIR_NAME, getAgentDir } from "../config.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 */
@@ -726,6 +728,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,
+2
View File
@@ -26,6 +26,7 @@ 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";
@@ -617,6 +618,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
@@ -11,6 +11,7 @@ import {
Spacer,
Text,
} from "@earendil-works/pi-tui";
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";
@@ -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;
@@ -71,6 +71,7 @@ import type {
ExtensionWidgetOptions,
} 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";
@@ -1566,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());
@@ -3846,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",
@@ -3904,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();
@@ -4891,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)) {
@@ -4,12 +4,12 @@ import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { type AssistantMessage, getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createTestResourceLoader } from "./utilities.js";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createTestResourceLoader } from "./utilities.ts";
vi.mock("../src/core/compaction/index.js", () => ({
calculateContextTokens: (usage: {
@@ -12,17 +12,17 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { AgentSession } from "../src/core/agent-session.js";
import type { AgentSession } from "../src/core/agent-session.ts";
import {
type AgentSessionRuntime,
type CreateAgentSessionRuntimeFactory,
createAgentSessionFromServices,
createAgentSessionRuntime,
createAgentSessionServices,
} from "../src/core/agent-session-runtime.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { SessionManager } from "../src/core/session-manager.js";
import { API_KEY } from "./utilities.js";
} from "../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { API_KEY } from "./utilities.ts";
describe.skipIf(!API_KEY)("AgentSession forking", () => {
let session: AgentSession;
@@ -13,13 +13,13 @@ import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createCodingTools } from "../src/index.js";
import { API_KEY, createTestResourceLoader } from "./utilities.js";
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createCodingTools } from "../src/index.ts";
import { API_KEY, createTestResourceLoader } from "./utilities.ts";
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
let session: AgentSession;
@@ -16,13 +16,13 @@ import {
} from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import type { BuildSystemPromptOptions } from "../src/core/system-prompt.js";
import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.js";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import type { BuildSystemPromptOptions } from "../src/core/system-prompt.ts";
import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.ts";
// Mock stream that mimics AssistantMessageEventStream
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
@@ -3,12 +3,12 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import type { ExtensionFactory } from "../src/core/sdk.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
import type { ExtensionFactory } from "../src/core/sdk.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
describe("AgentSession dynamic provider registration", () => {
let tempDir: string;
@@ -4,10 +4,10 @@ import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
describe("AgentSession dynamic tool registration", () => {
let tempDir: string;
@@ -5,12 +5,12 @@ import { Agent, type AgentEvent, type AgentTool } from "@earendil-works/pi-agent
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createTestResourceLoader } from "./utilities.js";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createTestResourceLoader } from "./utilities.ts";
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
@@ -8,16 +8,16 @@ import {
createAgentSessionFromServices,
createAgentSessionRuntime,
createAgentSessionServices,
} from "../src/core/agent-session-runtime.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { SessionManager } from "../src/core/session-manager.js";
} from "../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import type {
ExtensionFactory,
SessionBeforeForkEvent,
SessionBeforeSwitchEvent,
SessionShutdownEvent,
SessionStartEvent,
} from "../src/index.js";
} from "../src/index.ts";
type RecordedSessionEvent =
| SessionBeforeSwitchEvent
@@ -1,12 +1,12 @@
import { Agent } from "@earendil-works/pi-agent-core";
import { type AssistantMessage, getModel, type Usage } from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createTestResourceLoader } from "./utilities.js";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createTestResourceLoader } from "./utilities.ts";
const model = getModel("anthropic", "claude-sonnet-4-5")!;
@@ -10,7 +10,7 @@
*/
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { API_KEY, createTestSession, type TestSessionContext } from "./utilities.js";
import { API_KEY, createTestSession, type TestSessionContext } from "./utilities.ts";
describe.skipIf(!API_KEY)("AgentSession tree navigation e2e", () => {
let ctx: TestSessionContext;
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { stripAnsi } from "../src/utils/ansi.js";
import { stripAnsi } from "../src/utils/ansi.ts";
function referenceAnsiRegex(): RegExp {
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { parseArgs } from "../src/cli/args.js";
import { parseArgs } from "../src/cli/args.ts";
describe("parseArgs", () => {
describe("--version flag", () => {
@@ -1,7 +1,7 @@
import type { AssistantMessage } from "@earendil-works/pi-ai";
import { describe, expect, test } from "vitest";
import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import { AssistantMessageComponent } from "../src/modes/interactive/components/assistant-message.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
const OSC133_ZONE_START = "\x1b]133;A\x07";
const OSC133_ZONE_END = "\x1b]133;B\x07";
@@ -4,8 +4,8 @@ import { join } from "node:path";
import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
import lockfile from "proper-lockfile";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { clearConfigValueCache } from "../src/core/resolve-config-value.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { clearConfigValueCache } from "../src/core/resolve-config-value.ts";
describe("AuthStorage", () => {
let tempDir: string;
@@ -3,8 +3,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { executeBashWithOperations } from "../src/core/bash-executor.js";
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
import { executeBashWithOperations } from "../src/core/bash-executor.ts";
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.ts";
function toBashSingleQuotedArg(value: string): string {
return `'${value.replace(/\\/g, "/").replace(/'/g, `'"'"'`)}'`;
@@ -4,8 +4,8 @@
*/
import { visibleWidth } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it } from "vitest";
import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
/** Minimal TUI stub that only exposes terminal.columns */
function createTuiStub(columns: number): { columns: number; stub: any } {
@@ -2,9 +2,9 @@ import { mkdirSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { processFileArguments } from "../src/cli/file-processor.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createReadTool } from "../src/core/tools/read.js";
import { processFileArguments } from "../src/cli/file-processor.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createReadTool } from "../src/core/tools/read.ts";
// 1x1 red PNG image as base64 (smallest valid PNG)
const TINY_PNG_BASE64 =
@@ -68,7 +68,7 @@ vi.mock("@mariozechner/clipboard", () => ({
describe("readClipboardImage BMP conversion", () => {
test("converts BMP to PNG on Wayland/WSLg", async () => {
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
// Simulate Wayland session (WSLg)
const image = await readClipboardImage({
@@ -70,7 +70,7 @@ describe("readClipboardImage", () => {
throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`);
});
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
const result = await readClipboardImage({ platform: "linux", env: { WAYLAND_DISPLAY: "1" } });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
@@ -101,7 +101,7 @@ describe("readClipboardImage", () => {
return spawnOk(Buffer.alloc(0));
});
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
const result = await readClipboardImage({ platform: "linux", env: { XDG_SESSION_TYPE: "wayland" } });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
@@ -138,7 +138,7 @@ describe("readClipboardImage", () => {
throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`);
});
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
const result = await readClipboardImage({ platform: "linux", env: { WSL_DISTRO_NAME: "Ubuntu" } });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
@@ -153,7 +153,7 @@ describe("readClipboardImage", () => {
mocks.clipboard.hasImage.mockReturnValue(true);
mocks.clipboard.getImageBinary.mockResolvedValue(new Uint8Array([7]));
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
const result = await readClipboardImage({ platform: "linux", env: {} });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
@@ -167,7 +167,7 @@ describe("readClipboardImage", () => {
mocks.clipboard.hasImage.mockReturnValue(false);
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const { readClipboardImage } = await import("../src/utils/clipboard-image.ts");
const result = await readClipboardImage({ platform: "linux", env: {} });
expect(result).toBeNull();
});
+1 -1
View File
@@ -1,7 +1,7 @@
import { execSync, spawn } from "child_process";
import { platform } from "os";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { copyToClipboard } from "../src/utils/clipboard.js";
import { copyToClipboard } from "../src/utils/clipboard.ts";
const mocks = vi.hoisted(() => {
return {
@@ -3,7 +3,7 @@
*/
import { describe, expect, it } from "vitest";
import type { ExtensionAPI, SessionBeforeCompactEvent, SessionCompactEvent } from "../src/core/extensions/index.js";
import type { ExtensionAPI, SessionBeforeCompactEvent, SessionCompactEvent } from "../src/core/extensions/index.ts";
describe("Documentation example", () => {
it("custom compaction example should type-check correctly", () => {
@@ -8,21 +8,21 @@ import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import {
createExtensionRuntime,
type Extension,
type SessionBeforeCompactEvent,
type SessionCompactEvent,
type SessionEvent,
} from "../src/core/extensions/index.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
import { createCodingTools } from "../src/index.js";
import { createTestResourceLoader } from "./utilities.js";
} from "../src/core/extensions/index.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { createSyntheticSourceInfo } from "../src/core/source-info.ts";
import { createCodingTools } from "../src/index.ts";
import { createTestResourceLoader } from "./utilities.ts";
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
@@ -1,6 +1,6 @@
import type { Message } from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
import { serializeConversation } from "../src/core/compaction/utils.js";
import { serializeConversation } from "../src/core/compaction/utils.ts";
describe("serializeConversation", () => {
it("should truncate long tool results", () => {
@@ -1,7 +1,7 @@
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.js";
import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.ts";
const { completeSimpleMock } = vi.hoisted(() => ({
completeSimpleMock: vi.fn(),
@@ -14,7 +14,7 @@ import {
getLastAssistantUsage,
prepareCompaction,
shouldCompact,
} from "../src/core/compaction/index.js";
} from "../src/core/compaction/index.ts";
import {
buildSessionContext,
type CompactionEntry,
@@ -24,7 +24,7 @@ import {
type SessionEntry,
type SessionMessageEntry,
type ThinkingLevelChangeEntry,
} from "../src/core/session-manager.js";
} from "../src/core/session-manager.ts";
// ============================================================================
// Test fixtures
+38 -29
View File
@@ -7,7 +7,7 @@ import {
getSelfUpdateCommand,
getSelfUpdateUnavailableInstruction,
getUpdateInstruction,
} from "../src/config.js";
} from "../src/config.ts";
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");
const originalPath = process.env.PATH;
@@ -153,7 +153,7 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("pnpm");
expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe(
"Run: pnpm install -g @earendil-works/pi-coding-agent",
"Run: pnpm install -g --ignore-scripts @earendil-works/pi-coding-agent",
);
});
@@ -175,8 +175,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("npm");
expect(command).toEqual({
command: "npm",
args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"],
display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`,
args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"],
display: `npm --prefix ${prefix} install -g --ignore-scripts @earendil-works/pi-coding-agent`,
});
});
@@ -187,8 +187,8 @@ describe("detectInstallMethod", () => {
expect(command).toEqual({
command: "npm",
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g @new-scope/pi`,
args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g --ignore-scripts @new-scope/pi`,
steps: [
{
command: "npm",
@@ -197,8 +197,8 @@ describe("detectInstallMethod", () => {
},
{
command: "npm",
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
display: `npm --prefix ${prefix} install -g @new-scope/pi`,
args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: `npm --prefix ${prefix} install -g --ignore-scripts @new-scope/pi`,
},
],
});
@@ -211,8 +211,8 @@ describe("detectInstallMethod", () => {
expect(command).toEqual({
command: "npm",
args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"],
display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`,
args: ["--prefix", prefix, "install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"],
display: `npm --prefix ${prefix} install -g --ignore-scripts @earendil-works/pi-coding-agent`,
});
});
@@ -221,7 +221,14 @@ describe("detectInstallMethod", () => {
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", []);
expect(command?.args).toEqual(["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"]);
expect(command?.args).toEqual([
"--prefix",
prefix,
"install",
"-g",
"--ignore-scripts",
"@earendil-works/pi-coding-agent",
]);
});
test("quotes npm self-update display paths", () => {
@@ -229,7 +236,9 @@ describe("detectInstallMethod", () => {
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent");
expect(command?.display).toBe(`npm --prefix "${prefix}" install -g @earendil-works/pi-coding-agent`);
expect(command?.display).toBe(
`npm --prefix "${prefix}" install -g --ignore-scripts @earendil-works/pi-coding-agent`,
);
});
test("does not infer Windows npm custom prefixes from package paths", () => {
@@ -239,7 +248,7 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("npm");
expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe(
"Run: npm install -g @earendil-works/pi-coding-agent",
"Run: npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
);
});
@@ -251,8 +260,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("bun");
expect(command).toEqual({
command: "bun",
args: ["install", "-g", "@earendil-works/pi-coding-agent"],
display: "bun install -g @earendil-works/pi-coding-agent",
args: ["install", "-g", "--ignore-scripts", "@earendil-works/pi-coding-agent"],
display: "bun install -g --ignore-scripts @earendil-works/pi-coding-agent",
});
});
@@ -264,8 +273,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("pnpm");
expect(command).toEqual({
command: "pnpm",
args: ["install", "-g", "@new-scope/pi"],
display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g @new-scope/pi",
args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g --ignore-scripts @new-scope/pi",
steps: [
{
command: "pnpm",
@@ -274,8 +283,8 @@ describe("detectInstallMethod", () => {
},
{
command: "pnpm",
args: ["install", "-g", "@new-scope/pi"],
display: "pnpm install -g @new-scope/pi",
args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: "pnpm install -g --ignore-scripts @new-scope/pi",
},
],
});
@@ -319,8 +328,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("pnpm");
expect(command).toEqual({
command: "pnpm",
args: ["install", "-g", packageName],
display: `pnpm install -g ${packageName}`,
args: ["install", "-g", "--ignore-scripts", packageName],
display: `pnpm install -g --ignore-scripts ${packageName}`,
});
});
@@ -332,8 +341,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("yarn");
expect(command).toEqual({
command: "yarn",
args: ["global", "add", "@new-scope/pi"],
display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add @new-scope/pi",
args: ["global", "add", "--ignore-scripts", "@new-scope/pi"],
display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add --ignore-scripts @new-scope/pi",
steps: [
{
command: "yarn",
@@ -342,8 +351,8 @@ describe("detectInstallMethod", () => {
},
{
command: "yarn",
args: ["global", "add", "@new-scope/pi"],
display: "yarn global add @new-scope/pi",
args: ["global", "add", "--ignore-scripts", "@new-scope/pi"],
display: "yarn global add --ignore-scripts @new-scope/pi",
},
],
});
@@ -357,8 +366,8 @@ describe("detectInstallMethod", () => {
expect(detectInstallMethod()).toBe("bun");
expect(command).toEqual({
command: "bun",
args: ["install", "-g", "@new-scope/pi"],
display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g @new-scope/pi",
args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g --ignore-scripts @new-scope/pi",
steps: [
{
command: "bun",
@@ -367,8 +376,8 @@ describe("detectInstallMethod", () => {
},
{
command: "bun",
args: ["install", "-g", "@new-scope/pi"],
display: "bun install -g @new-scope/pi",
args: ["install", "-g", "--ignore-scripts", "@new-scope/pi"],
display: "bun install -g --ignore-scripts @new-scope/pi",
},
],
});
@@ -2,8 +2,8 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { ExtensionContext } from "../src/core/extensions/types.js";
import { createEditToolDefinition } from "../src/core/tools/edit.js";
import type { ExtensionContext } from "../src/core/extensions/types.ts";
import { createEditToolDefinition } from "../src/core/tools/edit.ts";
const tempDirs: string[] = [];
@@ -3,10 +3,10 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { Container, type Terminal, Text, TUI } from "@earendil-works/pi-tui";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { createEditToolDefinition } from "../src/core/tools/edit.js";
import { computeEditsDiff, type Edit } from "../src/core/tools/edit-diff.js";
import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import { createEditToolDefinition } from "../src/core/tools/edit.ts";
import { computeEditsDiff, type Edit } from "../src/core/tools/edit-diff.ts";
import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
class FakeTerminal implements Terminal {
columns = 80;
@@ -1,10 +1,10 @@
import type { Component } from "@earendil-works/pi-tui";
import { readFileSync } from "fs";
import { describe, expect, it } from "vitest";
import { ansiLinesToHtml } from "../src/core/export-html/ansi-to-html.js";
import { createToolHtmlRenderer } from "../src/core/export-html/tool-renderer.js";
import type { ToolDefinition } from "../src/core/extensions/types.js";
import type { Theme } from "../src/modes/interactive/theme/theme.js";
import { ansiLinesToHtml } from "../src/core/export-html/ansi-to-html.ts";
import { createToolHtmlRenderer } from "../src/core/export-html/tool-renderer.ts";
import type { ToolDefinition } from "../src/core/extensions/types.ts";
import type { Theme } from "../src/modes/interactive/theme/theme.ts";
describe("export HTML tool output whitespace", () => {
it("preserves whitespace for plain-text tool output lines without preserving template whitespace", () => {
@@ -3,7 +3,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.ts";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -440,7 +440,7 @@ describe("extensions discovery", () => {
fs.writeFileSync(explicitPath, extensionCodeWithTool("explicit"));
// Use loadExtensions directly to skip discovery
const { loadExtensions } = await import("../src/core/extensions/loader.js");
const { loadExtensions } = await import("../src/core/extensions/loader.ts");
const result = await loadExtensions([explicitPath], tempDir);
expect(result.errors).toHaveLength(0);
@@ -454,7 +454,7 @@ describe("extensions discovery", () => {
fs.writeFileSync(path.join(extensionsDir, "discovered.ts"), extensionCode);
// Use loadExtensions directly with empty paths
const { loadExtensions } = await import("../src/core/extensions/loader.js");
const { loadExtensions } = await import("../src/core/extensions/loader.ts");
const result = await loadExtensions([], tempDir);
expect(result.errors).toHaveLength(0);
@@ -2,11 +2,11 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
import { ExtensionRunner } from "../src/core/extensions/runner.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.ts";
import { ExtensionRunner } from "../src/core/extensions/runner.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
describe("Input Event", () => {
let tempDir: string;
@@ -6,13 +6,13 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
import { ExtensionRunner } from "../src/core/extensions/runner.js";
import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.js";
import { KeybindingsManager, type KeyId } from "../src/core/keybindings.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.ts";
import { ExtensionRunner } from "../src/core/extensions/runner.ts";
import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.ts";
import { KeybindingsManager, type KeyId } from "../src/core/keybindings.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
describe("ExtensionRunner", () => {
let tempDir: string;
@@ -2,9 +2,9 @@ import { access, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promi
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createEditTool } from "../src/core/tools/edit.js";
import { withFileMutationQueue } from "../src/core/tools/file-mutation-queue.js";
import { createWriteTool } from "../src/core/tools/write.js";
import { createEditTool } from "../src/core/tools/edit.ts";
import { withFileMutationQueue } from "../src/core/tools/file-mutation-queue.ts";
import { createWriteTool } from "../src/core/tools/write.ts";
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -37,7 +37,7 @@ vi.mock("child_process", () => ({
}),
}));
import { FooterDataProvider } from "../src/core/footer-data-provider.js";
import { FooterDataProvider } from "../src/core/footer-data-provider.ts";
type WorktreeFixture = {
worktreeDir: string;
@@ -1,9 +1,9 @@
import { visibleWidth } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, it } from "vitest";
import type { AgentSession } from "../src/core/agent-session.js";
import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.js";
import { FooterComponent } from "../src/modes/interactive/components/footer.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import type { AgentSession } from "../src/core/agent-session.ts";
import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.ts";
import { FooterComponent } from "../src/modes/interactive/components/footer.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
type AssistantUsage = {
input: number;
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parseFrontmatter, stripFrontmatter } from "../src/utils/frontmatter.js";
import { parseFrontmatter, stripFrontmatter } from "../src/utils/frontmatter.ts";
describe("parseFrontmatter", () => {
it("parses keys, strips quotes, and returns body", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parseGitUrl } from "../src/utils/git.js";
import { parseGitUrl } from "../src/utils/git.ts";
describe("Git URL Parsing", () => {
describe("protocol URLs (accepted without git: prefix)", () => {
@@ -12,8 +12,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultPackageManager } from "../src/core/package-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { DefaultPackageManager } from "../src/core/package-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
// Helper to run git commands in a directory
function git(args: string[], cwd: string): string {
@@ -3,8 +3,8 @@
*/
import { describe, expect, it } from "vitest";
import { convertToPng } from "../src/utils/image-convert.js";
import { formatDimensionNote, resizeImage } from "../src/utils/image-resize.js";
import { convertToPng } from "../src/utils/image-convert.ts";
import { formatDimensionNote, resizeImage } from "../src/utils/image-resize.ts";
// Small 2x2 red PNG image (base64) - generated with ImageMagick
const TINY_PNG =
@@ -8,9 +8,9 @@ vi.mock("../src/utils/image-resize.js", () => ({
formatDimensionNote: vi.fn(() => undefined),
}));
import { processFileArguments } from "../src/cli/file-processor.js";
import { createReadTool } from "../src/core/tools/read.js";
import { resizeImage } from "../src/utils/image-resize.js";
import { processFileArguments } from "../src/cli/file-processor.ts";
import { createReadTool } from "../src/core/tools/read.ts";
import { resizeImage } from "../src/utils/image-resize.ts";
const TINY_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import type { Args } from "../src/cli/args.js";
import { buildInitialMessage } from "../src/cli/initial-message.js";
import type { Args } from "../src/cli/args.ts";
import { buildInitialMessage } from "../src/cli/initial-message.ts";
function createArgs(messages: string[] = []): Args {
return {
@@ -1,5 +1,5 @@
import { describe, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
function createSettingsManager(warnings: { anthropicExtraUsage?: boolean } = {}) {
return {
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
type CloneCommandContext = {
sessionManager: { getLeafId: () => string | null };
@@ -1,5 +1,5 @@
import { describe, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
describe("InteractiveMode compaction events", () => {
test("rebuilds chat and appends a synthetic compaction summary at the bottom", async () => {
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { SessionImportFileNotFoundError } from "../src/core/agent-session-runtime.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { SessionImportFileNotFoundError } from "../src/core/agent-session-runtime.ts";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
type PathCommand = "/export" | "/import";
@@ -2,10 +2,10 @@ import { homedir } from "node:os";
import * as path from "node:path";
import { type AutocompleteProvider, CombinedAutocompleteProvider, Container } from "@earendil-works/pi-tui";
import { beforeAll, describe, expect, test, vi } from "vitest";
import type { AutocompleteProviderFactory } from "../src/core/extensions/types.js";
import type { SourceInfo } from "../src/core/source-info.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import type { AutocompleteProviderFactory } from "../src/core/extensions/types.ts";
import type { SourceInfo } from "../src/core/source-info.ts";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
function renderLastLine(container: Container, width = 120): string {
const last = container.children[container.children.length - 1];
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
type FakeUi = {
start: () => void;
@@ -2,9 +2,9 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { ENV_AGENT_DIR } from "../src/config.js";
import { KeybindingsManager } from "../src/core/keybindings.js";
import { runMigrations } from "../src/migrations.js";
import { ENV_AGENT_DIR } from "../src/config.ts";
import { KeybindingsManager } from "../src/core/keybindings.ts";
import { runMigrations } from "../src/migrations.ts";
describe("keybindings migration", () => {
const tempDirs: string[] = [];
@@ -5,8 +5,8 @@ import type { AnthropicMessagesCompat, Api, Context, Model, OpenAICompletionsCom
import { getApiProvider } from "@earendil-works/pi-ai";
import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts";
describe("ModelRegistry", () => {
let tempDir: string;
@@ -5,7 +5,7 @@ import {
findInitialModel,
parseModelPattern,
resolveCliModel,
} from "../src/core/model-resolver.js";
} from "../src/core/model-resolver.ts";
// Mock models for testing
const mockModels: Model<"anthropic-messages">[] = [
@@ -1,12 +1,12 @@
import { setKeybindings } from "@earendil-works/pi-tui";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { KeybindingsManager } from "../src/core/keybindings.js";
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.js";
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js";
import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import { stripAnsi } from "../src/utils/ansi.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { KeybindingsManager } from "../src/core/keybindings.ts";
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.ts";
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.ts";
import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
import { stripAnsi } from "../src/utils/ansi.ts";
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
@@ -2,8 +2,8 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "no
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.js";
import { main } from "../src/main.js";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import { main } from "../src/main.ts";
describe("package commands", () => {
let tempDir: string;
@@ -2,8 +2,8 @@ import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultPackageManager } from "../src/core/package-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { DefaultPackageManager } from "../src/core/package-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
describe("Package Manager git source parsing", () => {
let tempDir: string;
@@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
import { join, relative } from "node:path";
import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
function normalizeForMatch(value: string): string {
return value.replace(/\\/g, "/");
@@ -1747,7 +1747,7 @@ Content`,
// Main entry point
writeFileSync(
join(pkgDir, "extensions", "subagent", "index.ts"),
`import { helper } from "./agents.js";
`import { helper } from "./agents.ts";
export default function(api) { api.registerTool({ name: "test", description: "test", execute: async () => helper() }); }`,
);
// Helper module (should NOT be loaded as standalone extension)
@@ -1803,7 +1803,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
// Subdirectory with index.ts + helpers
writeFileSync(
join(pkgDir, "extensions", "complex", "index.ts"),
"import { a } from './a.js'; export default function(api) {}",
"import { a } from './a.ts'; export default function(api) {}",
);
writeFileSync(join(pkgDir, "extensions", "complex", "a.ts"), "export const a = 1;");
writeFileSync(join(pkgDir, "extensions", "complex", "b.ts"), "export const b = 2;");
@@ -2,7 +2,7 @@ import { mkdtempSync, readdirSync, rmdirSync, unlinkSync, writeFileSync } from "
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { expandPath, resolveReadPath, resolveToCwd } from "../src/core/tools/path-utils.js";
import { expandPath, resolveReadPath, resolveToCwd } from "../src/core/tools/path-utils.ts";
describe("path-utils", () => {
describe("expandPath", () => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.js";
import { canonicalizePath, getCwdRelativePath, isLocalPath } from "../src/utils/paths.ts";
let tempDir: string;
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getPiUserAgent } from "../src/utils/pi-user-agent.js";
import { getPiUserAgent } from "../src/utils/pi-user-agent.ts";
describe("getPiUserAgent", () => {
it("formats the user agent expected by pi.dev", () => {
@@ -6,7 +6,7 @@ import {
isSafeCommand,
markCompletedSteps,
type TodoItem,
} from "../examples/extensions/plan-mode/utils.js";
} from "../examples/extensions/plan-mode/utils.ts";
describe("isSafeCommand", () => {
describe("safe commands", () => {
@@ -1,7 +1,7 @@
import type { AssistantMessage, ImageContent } from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { SessionShutdownEvent } from "../src/index.js";
import { runPrintMode } from "../src/modes/print-mode.js";
import type { SessionShutdownEvent } from "../src/index.ts";
import { runPrintMode } from "../src/modes/print-mode.ts";
type EmitEvent = SessionShutdownEvent;
@@ -12,13 +12,13 @@ import { mkdirSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterAll, describe, expect, test } from "vitest";
import { getAgentDir } from "../src/config.js";
import { getAgentDir } from "../src/config.ts";
import {
expandPromptTemplate,
loadPromptTemplates,
parseCommandArgs,
substituteArgs,
} from "../src/core/prompt-templates.js";
} from "../src/core/prompt-templates.ts";
// ============================================================================
// substituteArgs
@@ -2,14 +2,14 @@ import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "nod
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ExtensionRunner } from "../src/core/extensions/runner.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import type { Skill } from "../src/core/skills.js";
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ExtensionRunner } from "../src/core/extensions/runner.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import type { Skill } from "../src/core/skills.ts";
import { createSyntheticSourceInfo } from "../src/core/source-info.ts";
describe("DefaultResourceLoader", () => {
let tempDir: string;
@@ -6,7 +6,7 @@ vi.mock("node:fs", () => ({
readFileSync,
}));
const { restoreSandboxEnv } = await import("../src/bun/restore-sandbox-env.js");
const { restoreSandboxEnv } = await import("../src/bun/restore-sandbox-env.ts");
describe("restoreSandboxEnv", () => {
it("does nothing when not running under bun", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { RpcClient } from "../src/modes/rpc/rpc-client.js";
import { RpcClient } from "../src/modes/rpc/rpc-client.ts";
type RpcClientPrivate = {
send: (command: { type: string }) => Promise<unknown>;
+1 -1
View File
@@ -1,7 +1,7 @@
import { dirname, join } from "node:path";
import * as readline from "node:readline";
import { fileURLToPath } from "node:url";
import { RpcClient } from "../src/modes/rpc/rpc-client.js";
import { RpcClient } from "../src/modes/rpc/rpc-client.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
+1 -1
View File
@@ -1,6 +1,6 @@
import { Readable } from "node:stream";
import { describe, expect, test } from "vitest";
import { attachJsonlLineReader, serializeJsonLine } from "../src/modes/rpc/jsonl.js";
import { attachJsonlLineReader, serializeJsonLine } from "../src/modes/rpc/jsonl.ts";
describe("RPC JSONL framing", () => {
test("serializes strict JSONL records without escaping Unicode separators", () => {
@@ -10,14 +10,14 @@ import {
type Model,
} from "@earendil-works/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AgentSession } from "../src/core/agent-session.js";
import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { runRpcMode } from "../src/modes/rpc/rpc-mode.js";
import { createTestResourceLoader } from "./utilities.js";
import { AgentSession } from "../src/core/agent-session.ts";
import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { runRpcMode } from "../src/modes/rpc/rpc-mode.ts";
import { createTestResourceLoader } from "./utilities.ts";
const rpcIo = vi.hoisted(() => ({
outputLines: [] as string[],
+1 -1
View File
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { AgentEvent } from "@earendil-works/pi-agent-core";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { RpcClient } from "../src/modes/rpc/rpc-client.js";
import { RpcClient } from "../src/modes/rpc/rpc-client.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -1,4 +1,4 @@
#!/usr/bin/env tsx
#!/usr/bin/env node
/**
* Manual SDK probe for OpenAI Codex prompt caching through the tool loop.
*
@@ -24,15 +24,15 @@ import {
import {
getOpenAICodexWebSocketDebugStats,
streamSimpleOpenAICodexResponses,
} from "../../ai/src/providers/openai-codex-responses.js";
import { AuthStorage } from "../src/core/auth-storage.js";
import { createExtensionRuntime } from "../src/core/extensions/loader.js";
import type { ToolDefinition } from "../src/core/extensions/types.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import type { ResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
} from "../../ai/src/providers/openai-codex-responses.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { createExtensionRuntime } from "../src/core/extensions/loader.ts";
import type { ToolDefinition } from "../src/core/extensions/types.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import type { ResourceLoader } from "../src/core/resource-loader.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
type Transport = "sse" | "websocket" | "websocket-cached" | "auto";
@@ -123,7 +123,7 @@ function parseArgs(argv: string[]): Args {
}
function printHelp(): void {
console.log(`Usage: npx tsx test/sdk-codex-cache-probe-tool-loop.ts [options]
console.log(`Usage: node test/sdk-codex-cache-probe-tool-loop.ts [options]
Options:
--turns <n> Number of turns to run. Must be between ${MIN_TURNS} and ${MAX_TURNS}. Default: ${DEFAULT_TURNS}
@@ -9,11 +9,11 @@ import {
type SimpleStreamOptions,
} from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
describe("createAgentSession OpenRouter attribution headers", () => {
let tempDir: string;
@@ -3,8 +3,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
describe("createAgentSession session manager defaults", () => {
let tempDir: string;
@@ -2,11 +2,11 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createExtensionRuntime } from "../src/core/extensions/loader.js";
import type { ResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { createSyntheticSourceInfo } from "../src/core/source-info.js";
import { createExtensionRuntime } from "../src/core/extensions/loader.ts";
import type { ResourceLoader } from "../src/core/resource-loader.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { createSyntheticSourceInfo } from "../src/core/source-info.ts";
describe("createAgentSession skills option", () => {
let tempDir: string;
@@ -2,9 +2,9 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "../src/core/agent-session-runtime.js";
import { getMissingSessionCwdIssue, MissingSessionCwdError } from "../src/core/session-cwd.js";
import { SessionManager } from "../src/core/session-manager.js";
import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "../src/core/agent-session-runtime.ts";
import { getMissingSessionCwdIssue, MissingSessionCwdError } from "../src/core/session-cwd.ts";
import { SessionManager } from "../src/core/session-manager.ts";
function createTempDir(name: string): string {
const dir = join(tmpdir(), `${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@@ -3,9 +3,9 @@ import { stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { SessionHeader } from "../src/core/session-manager.js";
import { SessionManager } from "../src/core/session-manager.js";
import { initTheme } from "../src/modes/interactive/theme/theme.js";
import type { SessionHeader } from "../src/core/session-manager.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
function createSessionFile(path: string): void {
const header: SessionHeader = {
@@ -7,7 +7,7 @@ import {
type SessionEntry,
type SessionMessageEntry,
type ThinkingLevelChangeEntry,
} from "../../src/core/session-manager.js";
} from "../../src/core/session-manager.ts";
function msg(id: string, parentId: string | null, role: "user" | "assistant", text: string): SessionMessageEntry {
const base = { type: "message" as const, id, parentId, timestamp: "2025-01-01T00:00:00Z" };
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { SessionManager } from "../../src/core/session-manager.js";
import { SessionManager } from "../../src/core/session-manager.ts";
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { findMostRecentSession, loadEntriesFromFile, SessionManager } from "../../src/core/session-manager.js";
import { findMostRecentSession, loadEntriesFromFile, SessionManager } from "../../src/core/session-manager.ts";
describe("loadEntriesFromFile", () => {
let tempDir: string;
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { type LabelEntry, SessionManager } from "../../src/core/session-manager.js";
import { type LabelEntry, SessionManager } from "../../src/core/session-manager.ts";
describe("SessionManager labels", () => {
it("sets and gets labels", () => {

Some files were not shown because too many files have changed in this diff Show More