mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
fix(coding-agent): remove process-cwd tool singletons and use tool-name allowlists
- switch SDK/CLI tool selection to name-based allowlists - apply allowlists across built-in, extension, and SDK tools - remove ambient process.cwd defaults from core tooling and resource helpers - update tests, examples, and TUI callers for explicit cwd plumbing - add regression coverage for extension tool filtering closes #3452 closes #2835
This commit is contained in:
@@ -741,11 +741,16 @@ Customize the inline working indicator shown while pi is streaming a response.
|
||||
|
||||
```typescript
|
||||
// Static indicator
|
||||
ctx.ui.setWorkingIndicator({ frames: ["●"] });
|
||||
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
|
||||
|
||||
// Custom animated indicator
|
||||
ctx.ui.setWorkingIndicator({
|
||||
frames: ["·", "•", "●", "•"],
|
||||
frames: [
|
||||
ctx.ui.theme.fg("dim", "·"),
|
||||
ctx.ui.theme.fg("muted", "•"),
|
||||
ctx.ui.theme.fg("accent", "●"),
|
||||
ctx.ui.theme.fg("muted", "•"),
|
||||
],
|
||||
intervalMs: 120,
|
||||
});
|
||||
|
||||
@@ -756,7 +761,7 @@ ctx.ui.setWorkingIndicator({ frames: [] });
|
||||
ctx.ui.setWorkingIndicator();
|
||||
```
|
||||
|
||||
This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling.
|
||||
This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling. Custom frames are rendered verbatim, so extensions must add their own colors when needed.
|
||||
|
||||
**Examples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)
|
||||
|
||||
|
||||
@@ -20,31 +20,49 @@ import type { ExtensionAPI, ExtensionContext, WorkingIndicatorOptions } from "@m
|
||||
|
||||
type WorkingIndicatorMode = "dot" | "none" | "pulse" | "spinner" | "default";
|
||||
|
||||
const SPINNER_INDICATOR: WorkingIndicatorOptions = {
|
||||
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||
intervalMs: 80,
|
||||
};
|
||||
const DOT_INDICATOR: WorkingIndicatorOptions = {
|
||||
frames: ["●"],
|
||||
};
|
||||
const PULSE_INDICATOR: WorkingIndicatorOptions = {
|
||||
frames: ["·", "•", "●", "•"],
|
||||
intervalMs: 120,
|
||||
};
|
||||
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
const PASTEL_RAINBOW = [
|
||||
"\x1b[38;2;255;179;186m",
|
||||
"\x1b[38;2;255;223;186m",
|
||||
"\x1b[38;2;255;255;186m",
|
||||
"\x1b[38;2;186;255;201m",
|
||||
"\x1b[38;2;186;225;255m",
|
||||
"\x1b[38;2;218;186;255m",
|
||||
];
|
||||
const RESET_FG = "\x1b[39m";
|
||||
const HIDDEN_INDICATOR: WorkingIndicatorOptions = {
|
||||
frames: [],
|
||||
};
|
||||
|
||||
function colorize(text: string, color: string): string {
|
||||
return `${color}${text}${RESET_FG}`;
|
||||
}
|
||||
|
||||
function getIndicator(mode: WorkingIndicatorMode): WorkingIndicatorOptions | undefined {
|
||||
switch (mode) {
|
||||
case "dot":
|
||||
return DOT_INDICATOR;
|
||||
return {
|
||||
frames: [colorize("●", PASTEL_RAINBOW[0])],
|
||||
};
|
||||
case "none":
|
||||
return HIDDEN_INDICATOR;
|
||||
case "pulse":
|
||||
return PULSE_INDICATOR;
|
||||
return {
|
||||
frames: [
|
||||
colorize("·", PASTEL_RAINBOW[0]),
|
||||
colorize("•", PASTEL_RAINBOW[2]),
|
||||
colorize("●", PASTEL_RAINBOW[4]),
|
||||
colorize("•", PASTEL_RAINBOW[5]),
|
||||
],
|
||||
intervalMs: 120,
|
||||
};
|
||||
case "spinner":
|
||||
return SPINNER_INDICATOR;
|
||||
return {
|
||||
frames: SPINNER_FRAMES.map((frame, index) =>
|
||||
colorize(frame, PASTEL_RAINBOW[index % PASTEL_RAINBOW.length]!),
|
||||
),
|
||||
intervalMs: 80,
|
||||
};
|
||||
case "default":
|
||||
return undefined;
|
||||
}
|
||||
@@ -66,7 +84,7 @@ function describeMode(mode: WorkingIndicatorMode): string {
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let mode: WorkingIndicatorMode = "dot";
|
||||
let mode: WorkingIndicatorMode = "spinner";
|
||||
|
||||
const applyIndicator = (ctx: ExtensionContext) => {
|
||||
ctx.ui.setWorkingIndicator(getIndicator(mode));
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
* Shows how to replace or modify the default system prompt.
|
||||
*/
|
||||
|
||||
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const agentDir = getAgentDir();
|
||||
|
||||
// Option 1: Replace prompt entirely
|
||||
const loader1 = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir,
|
||||
systemPromptOverride: () => `You are a helpful assistant that speaks like a pirate.
|
||||
Always end responses with "Arrr!"`,
|
||||
// Needed to avoid DefaultResourceLoader appending APPEND_SYSTEM.md from ~/.pi/agent or <cwd>/.pi.
|
||||
@@ -32,6 +37,8 @@ console.log("\n");
|
||||
|
||||
// Option 2: Append instructions to the default prompt
|
||||
const loader2 = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir,
|
||||
appendSystemPromptOverride: (base) => [
|
||||
...base,
|
||||
"## Additional Instructions\n- Always be concise\n- Use bullet points when listing things",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createAgentSession,
|
||||
createSyntheticSourceInfo,
|
||||
DefaultResourceLoader,
|
||||
getAgentDir,
|
||||
SessionManager,
|
||||
type Skill,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
@@ -24,6 +25,8 @@ const customSkill: Skill = {
|
||||
};
|
||||
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
skillsOverride: (current) => {
|
||||
const filteredSkills = current.skills.filter((s) => s.name.includes("browser") || s.name.includes("search"));
|
||||
return {
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
* export default function (pi: ExtensionAPI) { ... }
|
||||
*/
|
||||
|
||||
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Extensions are discovered automatically from standard locations.
|
||||
// You can also add paths via settings.json or DefaultResourceLoader options.
|
||||
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
additionalExtensionPaths: ["./my-logging-extension.ts", "./my-safety-extension.ts"],
|
||||
extensionFactories: [
|
||||
(pi) => {
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
* Context files provide project-specific instructions loaded into the system prompt.
|
||||
*/
|
||||
|
||||
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
// Disable context files entirely by returning an empty list in agentsFilesOverride.
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
agentsFilesOverride: (current) => ({
|
||||
agentsFiles: [
|
||||
...current.agentsFiles,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createAgentSession,
|
||||
createSyntheticSourceInfo,
|
||||
DefaultResourceLoader,
|
||||
getAgentDir,
|
||||
type PromptTemplate,
|
||||
SessionManager,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
@@ -26,6 +27,8 @@ const deployTemplate: PromptTemplate = {
|
||||
};
|
||||
|
||||
const loader = new DefaultResourceLoader({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptsOverride: (current) => ({
|
||||
prompts: [...current.prompts, deployTemplate],
|
||||
diagnostics: current.diagnostics,
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Load current settings (merged global + project)
|
||||
const settingsManagerFromDisk = SettingsManager.create();
|
||||
const settingsManagerFromDisk = SettingsManager.create(cwd);
|
||||
console.log("Current settings:", JSON.stringify(settingsManagerFromDisk.getGlobalSettings(), null, 2));
|
||||
|
||||
// Override specific settings
|
||||
const settingsManager = SettingsManager.create();
|
||||
const settingsManager = SettingsManager.create(cwd);
|
||||
settingsManager.applyOverrides({
|
||||
compaction: { enabled: false },
|
||||
retry: { enabled: true, maxRetries: 5, baseDelayMs: 1000 },
|
||||
|
||||
@@ -133,9 +133,6 @@ export class AgentSessionRuntime {
|
||||
}
|
||||
|
||||
private apply(result: CreateAgentSessionRuntimeResult): void {
|
||||
if (process.cwd() !== result.services.cwd) {
|
||||
process.chdir(result.services.cwd);
|
||||
}
|
||||
this._session = result.session;
|
||||
this._services = result.services;
|
||||
this._diagnostics = result.diagnostics;
|
||||
@@ -346,9 +343,6 @@ export async function createAgentSessionRuntime(
|
||||
): Promise<AgentSessionRuntime> {
|
||||
assertSessionCwdExists(options.sessionManager, options.cwd);
|
||||
const result = await createRuntime(options);
|
||||
if (process.cwd() !== result.services.cwd) {
|
||||
process.chdir(result.services.cwd);
|
||||
}
|
||||
return new AgentSessionRuntime(
|
||||
result.session,
|
||||
result.services,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { sanitizeBinaryOutput } from "../utils/shell.js";
|
||||
import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";
|
||||
import type { BashOperations } from "./tools/bash.js";
|
||||
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
||||
|
||||
// ============================================================================
|
||||
@@ -43,23 +43,6 @@ export interface BashResult {
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Execute a bash command with optional streaming and cancellation support.
|
||||
*
|
||||
* Uses the same local BashOperations backend as createBashTool() so interactive
|
||||
* user bash and tool-invoked bash share the same process spawning behavior.
|
||||
* Sanitization, newline normalization, temp-file capture, and truncation still
|
||||
* happen in executeBashWithOperations(), so reusing the local backend does not
|
||||
* change output processing behavior.
|
||||
*
|
||||
* @param command - The bash command to execute
|
||||
* @param options - Optional streaming callback and abort signal
|
||||
* @returns Promise resolving to execution result
|
||||
*/
|
||||
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
||||
return executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a bash command using custom BashOperations.
|
||||
* Used for remote execution (SSH, containers, etc.).
|
||||
|
||||
@@ -104,7 +104,7 @@ export type TerminalInputHandler = (data: string) => { consume?: boolean; data?:
|
||||
|
||||
/** Working indicator configuration for the interactive streaming loader. */
|
||||
export interface WorkingIndicatorOptions {
|
||||
/** Animation frames. Use an empty array to hide the indicator entirely. */
|
||||
/** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
|
||||
frames?: string[];
|
||||
/** Frame interval in milliseconds for animated indicators. */
|
||||
intervalMs?: number;
|
||||
@@ -142,6 +142,7 @@ export interface ExtensionUIContext {
|
||||
* - Omit the argument to restore the default animated spinner.
|
||||
* - Use `frames: ["●"]` for a static indicator.
|
||||
* - Use `frames: []` to hide the indicator entirely.
|
||||
* - Custom frames are rendered as provided, so extensions must add their own colors.
|
||||
*/
|
||||
setWorkingIndicator(options?: WorkingIndicatorOptions): void;
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export class FooterDataProvider {
|
||||
private refreshPending = false;
|
||||
private disposed = false;
|
||||
|
||||
constructor(cwd: string = process.cwd()) {
|
||||
constructor(cwd: string) {
|
||||
this.cwd = cwd;
|
||||
this.gitPaths = findGitPaths(cwd);
|
||||
this.setupGitWatcher();
|
||||
|
||||
@@ -25,7 +25,7 @@ export {
|
||||
createAgentSessionFromServices,
|
||||
createAgentSessionServices,
|
||||
} from "./agent-session-services.js";
|
||||
export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations } from "./bash-executor.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";
|
||||
// Extensions system
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
|
||||
@@ -175,14 +175,14 @@ function loadTemplatesFromDir(dir: string, getSourceInfo: (filePath: string) =>
|
||||
}
|
||||
|
||||
export interface LoadPromptTemplatesOptions {
|
||||
/** Working directory for project-local templates. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory for global templates. Default: from getPromptsDir() */
|
||||
agentDir?: string;
|
||||
/** Explicit prompt template paths (files or directories) */
|
||||
promptPaths?: string[];
|
||||
/** Include default prompt directories. Default: true */
|
||||
includeDefaults?: boolean;
|
||||
/** Working directory for project-local templates. */
|
||||
cwd: string;
|
||||
/** Agent config directory for global templates. */
|
||||
agentDir: string;
|
||||
/** Explicit prompt template paths (files or directories). */
|
||||
promptPaths: string[];
|
||||
/** Include default prompt directories. */
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
@@ -204,11 +204,11 @@ function resolvePromptPath(p: string, cwd: string): string {
|
||||
* 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/
|
||||
* 3. Explicit prompt paths
|
||||
*/
|
||||
export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {
|
||||
const resolvedCwd = options.cwd ?? process.cwd();
|
||||
const resolvedAgentDir = options.agentDir ?? getPromptsDir();
|
||||
const promptPaths = options.promptPaths ?? [];
|
||||
const includeDefaults = options.includeDefaults ?? true;
|
||||
export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
const promptPaths = options.promptPaths;
|
||||
const includeDefaults = options.includeDefaults;
|
||||
|
||||
const templates: PromptTemplate[] = [];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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, getAgentDir } from "../config.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
|
||||
@@ -73,11 +73,12 @@ function loadContextFileFromDir(dir: string): { path: string; content: string }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadProjectContextFiles(
|
||||
options: { cwd?: string; agentDir?: string } = {},
|
||||
): Array<{ path: string; content: string }> {
|
||||
const resolvedCwd = options.cwd ?? process.cwd();
|
||||
const resolvedAgentDir = options.agentDir ?? getAgentDir();
|
||||
export function loadProjectContextFiles(options: {
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
}): Array<{ path: string; content: string }> {
|
||||
const resolvedCwd = options.cwd;
|
||||
const resolvedAgentDir = options.agentDir;
|
||||
|
||||
const contextFiles: Array<{ path: string; content: string }> = [];
|
||||
const seenPaths = new Set<string>();
|
||||
@@ -113,8 +114,8 @@ export function loadProjectContextFiles(
|
||||
}
|
||||
|
||||
export interface DefaultResourceLoaderOptions {
|
||||
cwd?: string;
|
||||
agentDir?: string;
|
||||
cwd: string;
|
||||
agentDir: string;
|
||||
settingsManager?: SettingsManager;
|
||||
eventBus?: EventBus;
|
||||
additionalExtensionPaths?: string[];
|
||||
@@ -204,8 +205,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private lastThemePaths: string[];
|
||||
|
||||
constructor(options: DefaultResourceLoaderOptions) {
|
||||
this.cwd = options.cwd ?? process.cwd();
|
||||
this.agentDir = options.agentDir ?? getAgentDir();
|
||||
this.cwd = options.cwd;
|
||||
this.agentDir = options.agentDir;
|
||||
this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir);
|
||||
this.eventBus = options.eventBus ?? createEventBus();
|
||||
this.packageManager = new DefaultPackageManager({
|
||||
|
||||
@@ -144,7 +144,7 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
private globalSettingsPath: string;
|
||||
private projectSettingsPath: string;
|
||||
|
||||
constructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) {
|
||||
constructor(cwd: string, agentDir: string) {
|
||||
this.globalSettingsPath = join(agentDir, "settings.json");
|
||||
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
|
||||
}
|
||||
@@ -256,7 +256,7 @@ export class SettingsManager {
|
||||
}
|
||||
|
||||
/** Create a SettingsManager that loads from files */
|
||||
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
||||
static create(cwd: string, agentDir: string = getAgentDir()): SettingsManager {
|
||||
const storage = new FileSettingsStorage(cwd, agentDir);
|
||||
return SettingsManager.fromStorage(storage);
|
||||
}
|
||||
|
||||
@@ -374,14 +374,14 @@ function escapeXml(str: string): string {
|
||||
}
|
||||
|
||||
export interface LoadSkillsOptions {
|
||||
/** Working directory for project-local skills. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Agent config directory for global skills. Default: ~/.pi/agent */
|
||||
agentDir?: string;
|
||||
/** Working directory for project-local skills. */
|
||||
cwd: string;
|
||||
/** Agent config directory for global skills. */
|
||||
agentDir: string;
|
||||
/** Explicit skill paths (files or directories) */
|
||||
skillPaths?: string[];
|
||||
/** Include default skills directories. Default: true */
|
||||
includeDefaults?: boolean;
|
||||
skillPaths: string[];
|
||||
/** Include default skills directories. */
|
||||
includeDefaults: boolean;
|
||||
}
|
||||
|
||||
function normalizePath(input: string): string {
|
||||
@@ -401,8 +401,8 @@ function resolveSkillPath(p: string, cwd: string): string {
|
||||
* Load skills from all configured locations.
|
||||
* Returns skills and any validation diagnostics.
|
||||
*/
|
||||
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
||||
const { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;
|
||||
export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
|
||||
const { cwd, agentDir, skillPaths, includeDefaults } = options;
|
||||
|
||||
// Resolve agentDir - if not provided, use default from config
|
||||
const resolvedAgentDir = agentDir ?? getAgentDir();
|
||||
|
||||
@@ -16,8 +16,8 @@ export interface BuildSystemPromptOptions {
|
||||
promptGuidelines?: string[];
|
||||
/** Text to append to system prompt. */
|
||||
appendSystemPrompt?: string;
|
||||
/** Working directory. Default: process.cwd() */
|
||||
cwd?: string;
|
||||
/** Working directory. */
|
||||
cwd: string;
|
||||
/** Pre-loaded context files. */
|
||||
contextFiles?: Array<{ path: string; content: string }>;
|
||||
/** Pre-loaded skills. */
|
||||
@@ -25,7 +25,7 @@ export interface BuildSystemPromptOptions {
|
||||
}
|
||||
|
||||
/** Build the system prompt with tools, guidelines, and context */
|
||||
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
||||
export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
|
||||
const {
|
||||
customPrompt,
|
||||
selectedTools,
|
||||
@@ -36,7 +36,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
||||
contextFiles: providedContextFiles,
|
||||
skills: providedSkills,
|
||||
} = options;
|
||||
const resolvedCwd = cwd ?? process.cwd();
|
||||
const resolvedCwd = cwd;
|
||||
const promptCwd = resolvedCwd.replace(/\\/g, "/");
|
||||
|
||||
const now = new Date();
|
||||
|
||||
@@ -444,7 +444,3 @@ export function createBashToolDefinition(
|
||||
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
||||
return wrapToolDefinition(createBashToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default bash tool using process.cwd() for backwards compatibility. */
|
||||
export const bashToolDefinition = createBashToolDefinition(process.cwd());
|
||||
export const bashTool = createBashTool(process.cwd());
|
||||
|
||||
@@ -485,7 +485,3 @@ export function createEditToolDefinition(
|
||||
export function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {
|
||||
return wrapToolDefinition(createEditToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default edit tool using process.cwd() for backwards compatibility. */
|
||||
export const editToolDefinition = createEditToolDefinition(process.cwd());
|
||||
export const editTool = createEditTool(process.cwd());
|
||||
|
||||
@@ -368,7 +368,3 @@ export function createFindToolDefinition(
|
||||
export function createFindTool(cwd: string, options?: FindToolOptions): AgentTool<typeof findSchema> {
|
||||
return wrapToolDefinition(createFindToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default find tool using process.cwd() for backwards compatibility. */
|
||||
export const findToolDefinition = createFindToolDefinition(process.cwd());
|
||||
export const findTool = createFindTool(process.cwd());
|
||||
|
||||
@@ -382,7 +382,3 @@ export function createGrepToolDefinition(
|
||||
export function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {
|
||||
return wrapToolDefinition(createGrepToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default grep tool using process.cwd() for backwards compatibility. */
|
||||
export const grepToolDefinition = createGrepToolDefinition(process.cwd());
|
||||
export const grepTool = createGrepTool(process.cwd());
|
||||
|
||||
@@ -227,7 +227,3 @@ export function createLsToolDefinition(
|
||||
export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {
|
||||
return wrapToolDefinition(createLsToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default ls tool using process.cwd() for backwards compatibility. */
|
||||
export const lsToolDefinition = createLsToolDefinition(process.cwd());
|
||||
export const lsTool = createLsTool(process.cwd());
|
||||
|
||||
@@ -271,7 +271,3 @@ export function createReadToolDefinition(
|
||||
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
|
||||
return wrapToolDefinition(createReadToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default read tool using process.cwd() for backwards compatibility. */
|
||||
export const readToolDefinition = createReadToolDefinition(process.cwd());
|
||||
export const readTool = createReadTool(process.cwd());
|
||||
|
||||
@@ -279,7 +279,3 @@ export function createWriteToolDefinition(
|
||||
export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {
|
||||
return wrapToolDefinition(createWriteToolDefinition(cwd, options));
|
||||
}
|
||||
|
||||
/** Default write tool using process.cwd() for backwards compatibility. */
|
||||
export const writeToolDefinition = createWriteToolDefinition(process.cwd());
|
||||
export const writeTool = createWriteTool(process.cwd());
|
||||
|
||||
@@ -183,8 +183,6 @@ export {
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
type PromptTemplate,
|
||||
// Pre-built tools (use process.cwd())
|
||||
readOnlyTools,
|
||||
} from "./core/sdk.js";
|
||||
export {
|
||||
type BranchSummaryEntry,
|
||||
@@ -235,9 +233,6 @@ export {
|
||||
type BashToolDetails,
|
||||
type BashToolInput,
|
||||
type BashToolOptions,
|
||||
bashTool,
|
||||
bashToolDefinition,
|
||||
codingTools,
|
||||
createBashToolDefinition,
|
||||
createEditToolDefinition,
|
||||
createFindToolDefinition,
|
||||
@@ -252,33 +247,23 @@ export {
|
||||
type EditToolDetails,
|
||||
type EditToolInput,
|
||||
type EditToolOptions,
|
||||
editTool,
|
||||
editToolDefinition,
|
||||
type FindOperations,
|
||||
type FindToolDetails,
|
||||
type FindToolInput,
|
||||
type FindToolOptions,
|
||||
findTool,
|
||||
findToolDefinition,
|
||||
formatSize,
|
||||
type GrepOperations,
|
||||
type GrepToolDetails,
|
||||
type GrepToolInput,
|
||||
type GrepToolOptions,
|
||||
grepTool,
|
||||
grepToolDefinition,
|
||||
type LsOperations,
|
||||
type LsToolDetails,
|
||||
type LsToolInput,
|
||||
type LsToolOptions,
|
||||
lsTool,
|
||||
lsToolDefinition,
|
||||
type ReadOperations,
|
||||
type ReadToolDetails,
|
||||
type ReadToolInput,
|
||||
type ReadToolOptions,
|
||||
readTool,
|
||||
readToolDefinition,
|
||||
type ToolsOptions,
|
||||
type TruncationOptions,
|
||||
type TruncationResult,
|
||||
@@ -289,8 +274,6 @@ export {
|
||||
type WriteToolInput,
|
||||
type WriteToolOptions,
|
||||
withFileMutationQueue,
|
||||
writeTool,
|
||||
writeToolDefinition,
|
||||
} from "./core/tools/index.js";
|
||||
// Main entry point
|
||||
export { type MainOptions, main } from "./main.js";
|
||||
|
||||
@@ -301,7 +301,7 @@ export async function showDeprecationWarnings(warnings: string[]): Promise<void>
|
||||
*
|
||||
* @returns Object with migration results and deprecation warnings
|
||||
*/
|
||||
export function runMigrations(cwd: string = process.cwd()): {
|
||||
export function runMigrations(cwd: string): {
|
||||
migratedAuthProviders: string[];
|
||||
deprecationWarnings: string[];
|
||||
} {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions/types.js";
|
||||
import { allToolDefinitions } from "../../../core/tools/index.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";
|
||||
@@ -45,14 +45,14 @@ export class ToolExecutionComponent extends Container {
|
||||
options: ToolExecutionOptions = {},
|
||||
toolDefinition: ToolDefinition<any, any> | undefined,
|
||||
ui: TUI,
|
||||
cwd: string = process.cwd(),
|
||||
cwd: string,
|
||||
) {
|
||||
super();
|
||||
this.toolName = toolName;
|
||||
this.toolCallId = toolCallId;
|
||||
this.args = args;
|
||||
this.toolDefinition = toolDefinition;
|
||||
this.builtInToolDefinition = allToolDefinitions[toolName as keyof typeof allToolDefinitions];
|
||||
this.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];
|
||||
this.showImages = options.showImages ?? true;
|
||||
this.ui = ui;
|
||||
this.cwd = cwd;
|
||||
|
||||
@@ -53,7 +53,7 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
||||
return cachedShellConfig;
|
||||
}
|
||||
|
||||
const settings = SettingsManager.create();
|
||||
const settings = SettingsManager.create(process.cwd());
|
||||
const customShellPath = settings.getShellPath();
|
||||
|
||||
// 1. Check user-specified shell path
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { codingTools } from "../src/core/tools/index.js";
|
||||
import { createCodingTools } from "../src/index.js";
|
||||
import { API_KEY, createTestResourceLoader } from "./utilities.js";
|
||||
|
||||
describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||
@@ -52,7 +52,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||
tools: codingTools,
|
||||
tools: createCodingTools(process.cwd()),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { executeBash } from "../src/core/bash-executor.js";
|
||||
import { createBashTool } from "../src/core/tools/bash.js";
|
||||
import { executeBashWithOperations } from "../src/core/bash-executor.js";
|
||||
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
|
||||
function toBashSingleQuotedArg(value: string): string {
|
||||
return `'${value.replace(/\\/g, "/").replace(/'/g, `'"'"'`)}'`;
|
||||
@@ -87,9 +87,15 @@ describe.skipIf(process.platform !== "win32")("Windows child-process close handl
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const result = await withTimeout(executeBash(command, { signal: controller.signal }), 3000, () => {
|
||||
controller.abort();
|
||||
});
|
||||
const result = await withTimeout(
|
||||
executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
3000,
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.output).toContain("child-exiting");
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
@@ -21,7 +21,7 @@ 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 { codingTools } from "../src/core/tools/index.js";
|
||||
import { createCodingTools } from "../src/index.js";
|
||||
import { createTestResourceLoader } from "./utilities.js";
|
||||
|
||||
const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
||||
@@ -92,7 +92,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||
tools: codingTools,
|
||||
tools: createCodingTools(process.cwd()),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
mkdirSync(nestedDir, { recursive: true });
|
||||
process.chdir(nestedDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(nestedDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
expect(vi.mocked(spawnSync)).not.toHaveBeenCalled();
|
||||
@@ -125,7 +125,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
const repoDir = createPlainReftableRepo(tempDir);
|
||||
process.chdir(repoDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(repoDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
expect(vi.mocked(spawnSync)).toHaveBeenCalledWith(
|
||||
@@ -146,7 +146,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
const { worktreeDir } = createReftableWorktree(tempDir);
|
||||
process.chdir(worktreeDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(worktreeDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
} finally {
|
||||
@@ -159,7 +159,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
process.chdir(repoDir);
|
||||
resolvedBranch = "";
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(repoDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("detached");
|
||||
} finally {
|
||||
@@ -171,7 +171,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
const { worktreeDir, reftableDir } = createReftableWorktree(tempDir);
|
||||
process.chdir(worktreeDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(worktreeDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
vi.mocked(spawnSync).mockClear();
|
||||
@@ -194,7 +194,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
const { worktreeDir, reftableDir } = createReftableWorktree(tempDir);
|
||||
process.chdir(worktreeDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(worktreeDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
vi.mocked(execFile).mockClear();
|
||||
@@ -215,7 +215,7 @@ describe("FooterDataProvider reftable branch detection", () => {
|
||||
const { worktreeDir, reftableDir } = createReftableWorktree(tempDir);
|
||||
process.chdir(worktreeDir);
|
||||
|
||||
const provider = new FooterDataProvider();
|
||||
const provider = new FooterDataProvider(worktreeDir);
|
||||
try {
|
||||
expect(provider.getGitBranch()).toBe("main");
|
||||
resolvedBranch = "foo";
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { loadPromptTemplates, parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js";
|
||||
|
||||
// ============================================================================
|
||||
@@ -406,6 +407,8 @@ You are given one or more GitHub PR URLs: $@`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
@@ -427,6 +430,8 @@ Wrap it. Additional instructions: $ARGUMENTS`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
@@ -447,6 +452,8 @@ Audit changelog entries for all commits since the last release.`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
@@ -467,6 +474,8 @@ Do something`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
@@ -487,6 +496,8 @@ Analyze GitHub issue(s): $ARGUMENTS`,
|
||||
);
|
||||
|
||||
const templates = loadPromptTemplates({
|
||||
cwd: process.cwd(),
|
||||
agentDir: getAgentDir(),
|
||||
promptPaths: [testDir],
|
||||
includeDefaults: false,
|
||||
});
|
||||
|
||||
@@ -354,6 +354,7 @@ describe("skills", () => {
|
||||
agentDir: emptyAgentDir,
|
||||
cwd: emptyCwd,
|
||||
skillPaths: [join(fixturesDir, "valid-skill")],
|
||||
includeDefaults: true,
|
||||
});
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].sourceInfo.scope).toBe("temporary");
|
||||
@@ -365,6 +366,7 @@ describe("skills", () => {
|
||||
agentDir: emptyAgentDir,
|
||||
cwd: emptyCwd,
|
||||
skillPaths: ["/non/existent/path"],
|
||||
includeDefaults: true,
|
||||
});
|
||||
expect(skills).toHaveLength(0);
|
||||
expect(diagnostics.some((d: ResourceDiagnostic) => d.message.includes("does not exist"))).toBe(true);
|
||||
@@ -376,11 +378,13 @@ describe("skills", () => {
|
||||
agentDir: emptyAgentDir,
|
||||
cwd: emptyCwd,
|
||||
skillPaths: ["~/.pi/agent/skills"],
|
||||
includeDefaults: true,
|
||||
});
|
||||
const { skills: withoutTilde } = loadSkills({
|
||||
agentDir: emptyAgentDir,
|
||||
cwd: emptyCwd,
|
||||
skillPaths: [homeSkillsDir],
|
||||
includeDefaults: true,
|
||||
});
|
||||
expect(withTilde.length).toBe(withoutTilde.length);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ describe("buildSystemPrompt", () => {
|
||||
selectedTools: [],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Available tools:\n(none)");
|
||||
@@ -18,6 +19,7 @@ describe("buildSystemPrompt", () => {
|
||||
selectedTools: [],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Show file paths clearly");
|
||||
@@ -35,6 +37,7 @@ describe("buildSystemPrompt", () => {
|
||||
},
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain("- read:");
|
||||
@@ -53,6 +56,7 @@ describe("buildSystemPrompt", () => {
|
||||
},
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
||||
@@ -63,6 +67,7 @@ describe("buildSystemPrompt", () => {
|
||||
selectedTools: ["read", "dynamic_tool"],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).not.toContain("dynamic_tool");
|
||||
@@ -76,6 +81,7 @@ describe("buildSystemPrompt", () => {
|
||||
promptGuidelines: ["Use dynamic_tool for project summaries."],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt).toContain("- Use dynamic_tool for project summaries.");
|
||||
@@ -87,6 +93,7 @@ describe("buildSystemPrompt", () => {
|
||||
promptGuidelines: ["Use dynamic_tool for summaries.", " Use dynamic_tool for summaries. ", " "],
|
||||
contextFiles: [],
|
||||
skills: [],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(prompt.match(/- Use dynamic_tool for summaries\./g)).toHaveLength(1);
|
||||
|
||||
@@ -40,7 +40,15 @@ describe("ToolExecutionComponent parity", () => {
|
||||
renderResult: () => new Text("custom result", 0, 0),
|
||||
};
|
||||
|
||||
const component = new ToolExecutionComponent("custom_tool", "tool-1", {}, {}, toolDefinition, createFakeTui());
|
||||
const component = new ToolExecutionComponent(
|
||||
"custom_tool",
|
||||
"tool-1",
|
||||
{},
|
||||
{},
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
expect(stripAnsi(component.render(120).join("\n"))).toContain("custom call");
|
||||
|
||||
component.updateResult(
|
||||
@@ -69,6 +77,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
overrideDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [], details: { diff: "+1 after", firstChangedLine: 1 }, isError: false });
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -85,6 +94,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
undefined,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
expect(rendered).toContain("read");
|
||||
@@ -119,6 +129,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
createReadToolDefinition(process.cwd()),
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "hello" }], details: undefined, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -138,6 +149,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
overrideDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "hello" }], details: undefined, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -158,6 +170,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
overrideDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "hello" }], details: undefined, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -179,6 +192,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
renderResult: () => new Text("override result", 0, 0),
|
||||
},
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "hello" }], details: undefined, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -201,6 +215,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
renderResult: () => new Text("wrapped override result", 0, 0),
|
||||
},
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "hello" }], details: undefined, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -221,7 +236,15 @@ describe("ToolExecutionComponent parity", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const component = new ToolExecutionComponent("custom_tool", "tool-5", {}, {}, toolDefinition, createFakeTui());
|
||||
const component = new ToolExecutionComponent(
|
||||
"custom_tool",
|
||||
"tool-5",
|
||||
{},
|
||||
{},
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "done" }], details: {}, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
expect(rendered).toContain("custom call shared-token");
|
||||
@@ -243,6 +266,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "done" }], details: {}, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -261,6 +285,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult({ content: [{ type: "text", text: "done" }], details: {}, isError: false }, false);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
@@ -276,6 +301,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
createWriteToolDefinition(process.cwd()),
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||
expect(rendered).toContain("one");
|
||||
@@ -291,6 +317,7 @@ describe("ToolExecutionComponent parity", () => {
|
||||
{},
|
||||
createReadToolDefinition(process.cwd()),
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
component.updateResult(
|
||||
{ content: [{ type: "text", text: "one\ntwo\n" }], details: undefined, isError: false },
|
||||
|
||||
@@ -2,16 +2,26 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeBash } from "../src/core/bash-executor.js";
|
||||
import { bashTool, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import { editTool } from "../src/core/tools/edit.js";
|
||||
import { findTool } from "../src/core/tools/find.js";
|
||||
import { grepTool } from "../src/core/tools/grep.js";
|
||||
import { lsTool } from "../src/core/tools/ls.js";
|
||||
import { readTool } from "../src/core/tools/read.js";
|
||||
import { writeTool } from "../src/core/tools/write.js";
|
||||
import { executeBashWithOperations } from "../src/core/bash-executor.js";
|
||||
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import {
|
||||
createEditTool,
|
||||
createFindTool,
|
||||
createGrepTool,
|
||||
createLsTool,
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
} from "../src/index.js";
|
||||
import * as shellModule from "../src/utils/shell.js";
|
||||
|
||||
const readTool = createReadTool(process.cwd());
|
||||
const writeTool = createWriteTool(process.cwd());
|
||||
const editTool = createEditTool(process.cwd());
|
||||
const bashTool = createBashTool(process.cwd());
|
||||
const grepTool = createGrepTool(process.cwd());
|
||||
const findTool = createFindTool(process.cwd());
|
||||
const lsTool = createLsTool(process.cwd());
|
||||
|
||||
// Helper to extract text from content blocks
|
||||
function getTextOutput(result: any): string {
|
||||
return (
|
||||
@@ -438,7 +448,11 @@ describe("Coding Agent Tools", () => {
|
||||
});
|
||||
|
||||
it("should preserve executeBash sanitization when using local bash operations", async () => {
|
||||
const result = await executeBash("printf '\\033[31mred\\033[0m\\r\\n'");
|
||||
const result = await executeBashWithOperations(
|
||||
"printf '\\033[31mred\\033[0m\\r\\n'",
|
||||
process.cwd(),
|
||||
createLocalBashOperations(),
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toBe("red\n");
|
||||
@@ -468,7 +482,7 @@ describe("Coding Agent Tools", () => {
|
||||
});
|
||||
|
||||
it("executeBash should persist full output when truncation happens by line count only", async () => {
|
||||
const result = await executeBash("seq 3000");
|
||||
const result = await executeBashWithOperations("seq 3000", process.cwd(), createLocalBashOperations());
|
||||
const fullOutputPath = result.fullOutputPath;
|
||||
|
||||
expect(result.truncated).toBe(true);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import type { ResourceLoader } from "../src/core/resource-loader.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
import { codingTools } from "../src/core/tools/index.js";
|
||||
import { createCodingTools } from "../src/index.js";
|
||||
|
||||
/**
|
||||
* API key for authenticated tests. Tests using this should be wrapped in
|
||||
@@ -242,7 +242,7 @@ export function createTestSession(options: TestSessionOptions = {}): TestSession
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: options.systemPrompt ?? "You are a helpful assistant. Be extremely concise.",
|
||||
tools: codingTools,
|
||||
tools: createCodingTools(process.cwd()),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -265,11 +265,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
||||
private basePath: string;
|
||||
private fdPath: string | null;
|
||||
|
||||
constructor(
|
||||
commands: (SlashCommand | AutocompleteItem)[] = [],
|
||||
basePath: string = process.cwd(),
|
||||
fdPath: string | null = null,
|
||||
) {
|
||||
constructor(commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string, fdPath: string | null = null) {
|
||||
this.commands = commands;
|
||||
this.basePath = basePath;
|
||||
this.fdPath = fdPath;
|
||||
|
||||
@@ -20,6 +20,7 @@ export class Loader extends Text {
|
||||
private currentFrame = 0;
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
private ui: TUI | null = null;
|
||||
private renderIndicatorVerbatim = false;
|
||||
|
||||
constructor(
|
||||
ui: TUI,
|
||||
@@ -55,7 +56,8 @@ export class Loader extends Text {
|
||||
}
|
||||
|
||||
setIndicator(indicator?: LoaderIndicatorOptions): void {
|
||||
this.frames = indicator?.frames ? [...indicator.frames] : [...DEFAULT_FRAMES];
|
||||
this.renderIndicatorVerbatim = indicator !== undefined;
|
||||
this.frames = indicator?.frames !== undefined ? [...indicator.frames] : [...DEFAULT_FRAMES];
|
||||
this.intervalMs = indicator?.intervalMs && indicator.intervalMs > 0 ? indicator.intervalMs : DEFAULT_INTERVAL_MS;
|
||||
this.currentFrame = 0;
|
||||
this.start();
|
||||
@@ -74,7 +76,8 @@ export class Loader extends Text {
|
||||
|
||||
private updateDisplay(): void {
|
||||
const frame = this.frames[this.currentFrame] ?? "";
|
||||
const indicator = frame.length > 0 ? `${this.spinnerColorFn(frame)} ` : "";
|
||||
const renderedFrame = this.renderIndicatorVerbatim ? frame : this.spinnerColorFn(frame);
|
||||
const indicator = frame.length > 0 ? `${renderedFrame} ` : "";
|
||||
this.setText(`${indicator}${this.messageColorFn(this.message)}`);
|
||||
if (this.ui) {
|
||||
this.ui.requestRender();
|
||||
|
||||
@@ -2476,14 +2476,17 @@ describe("Editor component", () => {
|
||||
|
||||
it("awaits async slash command argument completions", async () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const provider = new CombinedAutocompleteProvider([
|
||||
{
|
||||
name: "load-skills",
|
||||
description: "Load skills",
|
||||
getArgumentCompletions: async (prefix) =>
|
||||
prefix.startsWith("s") ? [{ value: "skill-a", label: "skill-a" }] : null,
|
||||
},
|
||||
]);
|
||||
const provider = new CombinedAutocompleteProvider(
|
||||
[
|
||||
{
|
||||
name: "load-skills",
|
||||
description: "Load skills",
|
||||
getArgumentCompletions: async (prefix) =>
|
||||
prefix.startsWith("s") ? [{ value: "skill-a", label: "skill-a" }] : null,
|
||||
},
|
||||
],
|
||||
process.cwd(),
|
||||
);
|
||||
editor.setAutocompleteProvider(provider);
|
||||
editor.setText("/load-skills ");
|
||||
|
||||
@@ -2498,15 +2501,18 @@ describe("Editor component", () => {
|
||||
|
||||
it("ignores invalid slash command argument completion results", async () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const provider = new CombinedAutocompleteProvider([
|
||||
{
|
||||
name: "load-skills",
|
||||
description: "Load skills",
|
||||
getArgumentCompletions: (() => "not-an-array") as unknown as (
|
||||
argumentPrefix: string,
|
||||
) => Promise<{ value: string; label: string }[] | null>,
|
||||
},
|
||||
]);
|
||||
const provider = new CombinedAutocompleteProvider(
|
||||
[
|
||||
{
|
||||
name: "load-skills",
|
||||
description: "Load skills",
|
||||
getArgumentCompletions: (() => "not-an-array") as unknown as (
|
||||
argumentPrefix: string,
|
||||
) => Promise<{ value: string; label: string }[] | null>,
|
||||
},
|
||||
],
|
||||
process.cwd(),
|
||||
);
|
||||
editor.setAutocompleteProvider(provider);
|
||||
editor.setText("/load-skills ");
|
||||
|
||||
@@ -2518,14 +2524,17 @@ describe("Editor component", () => {
|
||||
|
||||
it("does not show argument completions when command has no argument completer", async () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
const provider = new CombinedAutocompleteProvider([
|
||||
{ name: "help", description: "Show help" },
|
||||
{
|
||||
name: "model",
|
||||
description: "Switch model",
|
||||
getArgumentCompletions: () => [{ value: "claude-opus", label: "claude-opus" }],
|
||||
},
|
||||
]);
|
||||
const provider = new CombinedAutocompleteProvider(
|
||||
[
|
||||
{ name: "help", description: "Show help" },
|
||||
{
|
||||
name: "model",
|
||||
description: "Switch model",
|
||||
getArgumentCompletions: () => [{ value: "claude-opus", label: "claude-opus" }],
|
||||
},
|
||||
],
|
||||
process.cwd(),
|
||||
);
|
||||
editor.setAutocompleteProvider(provider);
|
||||
|
||||
editor.handleInput("/");
|
||||
|
||||
Reference in New Issue
Block a user