diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index 59f7c11dd..12666af4f 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -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) diff --git a/packages/coding-agent/examples/extensions/working-indicator.ts b/packages/coding-agent/examples/extensions/working-indicator.ts index e7fd29c86..53ce1a735 100644 --- a/packages/coding-agent/examples/extensions/working-indicator.ts +++ b/packages/coding-agent/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)); diff --git a/packages/coding-agent/examples/sdk/03-custom-prompt.ts b/packages/coding-agent/examples/sdk/03-custom-prompt.ts index 7a4444d7d..eab00d842 100644 --- a/packages/coding-agent/examples/sdk/03-custom-prompt.ts +++ b/packages/coding-agent/examples/sdk/03-custom-prompt.ts @@ -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 /.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", diff --git a/packages/coding-agent/examples/sdk/04-skills.ts b/packages/coding-agent/examples/sdk/04-skills.ts index 9ed9696b1..852c2c199 100644 --- a/packages/coding-agent/examples/sdk/04-skills.ts +++ b/packages/coding-agent/examples/sdk/04-skills.ts @@ -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 { diff --git a/packages/coding-agent/examples/sdk/06-extensions.ts b/packages/coding-agent/examples/sdk/06-extensions.ts index 2d03d387d..ddc0ee385 100644 --- a/packages/coding-agent/examples/sdk/06-extensions.ts +++ b/packages/coding-agent/examples/sdk/06-extensions.ts @@ -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) => { diff --git a/packages/coding-agent/examples/sdk/07-context-files.ts b/packages/coding-agent/examples/sdk/07-context-files.ts index a0240659c..d49e32ecd 100644 --- a/packages/coding-agent/examples/sdk/07-context-files.ts +++ b/packages/coding-agent/examples/sdk/07-context-files.ts @@ -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, diff --git a/packages/coding-agent/examples/sdk/08-prompt-templates.ts b/packages/coding-agent/examples/sdk/08-prompt-templates.ts index b47aebb7a..6aec91039 100644 --- a/packages/coding-agent/examples/sdk/08-prompt-templates.ts +++ b/packages/coding-agent/examples/sdk/08-prompt-templates.ts @@ -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, diff --git a/packages/coding-agent/examples/sdk/10-settings.ts b/packages/coding-agent/examples/sdk/10-settings.ts index b2abf7e4f..bbfdea9d1 100644 --- a/packages/coding-agent/examples/sdk/10-settings.ts +++ b/packages/coding-agent/examples/sdk/10-settings.ts @@ -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 }, diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index 72691ffea..5f3e8d83c 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -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 { 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, diff --git a/packages/coding-agent/src/core/bash-executor.ts b/packages/coding-agent/src/core/bash-executor.ts index 2077b5ec9..3c185e21d 100644 --- a/packages/coding-agent/src/core/bash-executor.ts +++ b/packages/coding-agent/src/core/bash-executor.ts @@ -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 { - return executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), options); -} - /** * Execute a bash command using custom BashOperations. * Used for remote execution (SSH, containers, etc.). diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 87c24e240..f7852fa43 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -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; diff --git a/packages/coding-agent/src/core/footer-data-provider.ts b/packages/coding-agent/src/core/footer-data-provider.ts index c76520fed..120caa0b0 100644 --- a/packages/coding-agent/src/core/footer-data-provider.ts +++ b/packages/coding-agent/src/core/footer-data-provider.ts @@ -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(); diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index 3fa96f95a..d4faf8e2c 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -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 diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 4850351fc..8527f9111 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -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[] = []; diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index c1c49b235..9f07787e2 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -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(); @@ -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({ diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index d1f3493f2..8700029ad 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -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); } diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index fc6077f9f..ae0e07e84 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -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(); diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index d5c445e41..6e4749926 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -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(); diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 0a5e0a585..6c425fd9b 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -444,7 +444,3 @@ export function createBashToolDefinition( export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index e4bba04de..44a38ad02 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -485,7 +485,3 @@ export function createEditToolDefinition( export function createEditTool(cwd: string, options?: EditToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 0a8cbc2ad..4ca5e2c54 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -368,7 +368,3 @@ export function createFindToolDefinition( export function createFindTool(cwd: string, options?: FindToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index a9c072823..46928c392 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -382,7 +382,3 @@ export function createGrepToolDefinition( export function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/ls.ts b/packages/coding-agent/src/core/tools/ls.ts index 68dd9eef1..cb903250c 100644 --- a/packages/coding-agent/src/core/tools/ls.ts +++ b/packages/coding-agent/src/core/tools/ls.ts @@ -227,7 +227,3 @@ export function createLsToolDefinition( export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 4057fc38e..26095550c 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -271,7 +271,3 @@ export function createReadToolDefinition( export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index a03da5f6c..af07bdda5 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -279,7 +279,3 @@ export function createWriteToolDefinition( export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool { 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()); diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 65f05f5da..1240f7c16 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -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"; diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index fef5eb640..5b6061aca 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -301,7 +301,7 @@ export async function showDeprecationWarnings(warnings: string[]): Promise * * @returns Object with migration results and deprecation warnings */ -export function runMigrations(cwd: string = process.cwd()): { +export function runMigrations(cwd: string): { migratedAuthProviders: string[]; deprecationWarnings: string[]; } { diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 283e5127f..c6ed23744 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -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 | 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; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 9cd545979..1e9b5ed5b 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -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 diff --git a/packages/coding-agent/test/agent-session-compaction.test.ts b/packages/coding-agent/test/agent-session-compaction.test.ts index feba45eef..307518f32 100644 --- a/packages/coding-agent/test/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/agent-session-compaction.test.ts @@ -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()), }, }); diff --git a/packages/coding-agent/test/bash-close-hang-windows.test.ts b/packages/coding-agent/test/bash-close-hang-windows.test.ts index ef8c79225..10ae6414e 100644 --- a/packages/coding-agent/test/bash-close-hang-windows.test.ts +++ b/packages/coding-agent/test/bash-close-hang-windows.test.ts @@ -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); diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index b366a4793..48d039390 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -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()), }, }); diff --git a/packages/coding-agent/test/footer-data-provider.test.ts b/packages/coding-agent/test/footer-data-provider.test.ts index c3d90532f..8a999fab4 100644 --- a/packages/coding-agent/test/footer-data-provider.test.ts +++ b/packages/coding-agent/test/footer-data-provider.test.ts @@ -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"; diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index bef43f44b..67be0da4e 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -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, }); diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index f7426b30a..84803e6b1 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -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); }); diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index ce60df9ae..31a5d6b42 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -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); diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 1bfb0ec27..b9b597eb4 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -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 }, diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 5cb122790..2d45dbae5 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -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); diff --git a/packages/coding-agent/test/utilities.ts b/packages/coding-agent/test/utilities.ts index 5ede0a574..b5113be1d 100644 --- a/packages/coding-agent/test/utilities.ts +++ b/packages/coding-agent/test/utilities.ts @@ -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()), }, }); diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 4ff1b6b73..7137d5f4f 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -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; diff --git a/packages/tui/src/components/loader.ts b/packages/tui/src/components/loader.ts index 780363d57..6bac6ce81 100644 --- a/packages/tui/src/components/loader.ts +++ b/packages/tui/src/components/loader.ts @@ -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(); diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 51566eb9f..666d90590 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -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("/");