@oh-my-pi/pi-coding-agent 12.1.1 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/examples/sdk/11-sessions.ts +1 -1
  3. package/package.json +7 -7
  4. package/src/capability/index.ts +2 -1
  5. package/src/capability/types.ts +1 -1
  6. package/src/cli/file-processor.ts +2 -1
  7. package/src/cli/shell-cli.ts +2 -2
  8. package/src/commit/agentic/index.ts +2 -1
  9. package/src/commit/pipeline.ts +2 -1
  10. package/src/config/prompt-templates.ts +3 -3
  11. package/src/config/settings-schema.ts +11 -0
  12. package/src/config/settings.ts +2 -2
  13. package/src/config.ts +6 -6
  14. package/src/debug/system-info.ts +2 -2
  15. package/src/extensibility/custom-commands/loader.ts +5 -5
  16. package/src/extensibility/plugins/installer.ts +2 -2
  17. package/src/extensibility/plugins/manager.ts +2 -1
  18. package/src/extensibility/skills.ts +3 -2
  19. package/src/extensibility/slash-commands.ts +1 -1
  20. package/src/ipy/executor.ts +2 -2
  21. package/src/ipy/modules.ts +3 -3
  22. package/src/main.ts +9 -7
  23. package/src/mcp/transports/stdio.ts +2 -1
  24. package/src/modes/components/footer.ts +3 -2
  25. package/src/modes/components/oauth-selector.ts +96 -21
  26. package/src/modes/components/status-line/segments.ts +2 -1
  27. package/src/modes/components/status-line.ts +2 -1
  28. package/src/modes/components/tool-execution.ts +2 -1
  29. package/src/modes/controllers/command-controller.ts +60 -2
  30. package/src/modes/controllers/mcp-command-controller.ts +8 -8
  31. package/src/modes/controllers/selector-controller.ts +35 -11
  32. package/src/modes/interactive-mode.ts +2 -2
  33. package/src/sdk.ts +37 -11
  34. package/src/session/agent-session.ts +12 -0
  35. package/src/session/session-manager.ts +7 -4
  36. package/src/system-prompt.ts +41 -28
  37. package/src/tools/bash-normalize.ts +2 -2
  38. package/src/tools/bash.ts +2 -1
  39. package/src/tools/fetch.ts +3 -3
  40. package/src/tools/python.ts +2 -1
package/src/sdk.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
3
+ import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
3
4
  import type { Component } from "@oh-my-pi/pi-tui";
4
5
  import { $env, logger, postmortem } from "@oh-my-pi/pi-utils";
5
- import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
6
+ import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
6
7
  import chalk from "chalk";
7
8
  import { loadCapability } from "./capability";
8
9
  import { type Rule, ruleCapability } from "./capability/rule";
@@ -85,7 +86,7 @@ const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.w
85
86
 
86
87
  // Types
87
88
  export interface CreateAgentSessionOptions {
88
- /** Working directory for project-local discovery. Default: process.cwd() */
89
+ /** Working directory for project-local discovery. Default: getProjectDir() */
89
90
  cwd?: string;
90
91
  /** Global config directory. Default: ~/.omp/agent */
91
92
  agentDir?: string;
@@ -240,7 +241,7 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
240
241
  * Discover extensions from cwd.
241
242
  */
242
243
  export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsResult> {
243
- const resolvedCwd = cwd ?? process.cwd();
244
+ const resolvedCwd = cwd ?? getProjectDir();
244
245
 
245
246
  return discoverAndLoadExtensions([], resolvedCwd);
246
247
  }
@@ -255,7 +256,7 @@ export async function discoverSkills(
255
256
  ): Promise<{ skills: Skill[]; warnings: SkillWarning[] }> {
256
257
  return await loadSkillsInternal({
257
258
  ...settings,
258
- cwd: cwd ?? process.cwd(),
259
+ cwd: cwd ?? getProjectDir(),
259
260
  });
260
261
  }
261
262
 
@@ -268,7 +269,7 @@ export async function discoverContextFiles(
268
269
  _agentDir?: string,
269
270
  ): Promise<Array<{ path: string; content: string; depth?: number }>> {
270
271
  return await loadContextFilesInternal({
271
- cwd: cwd ?? process.cwd(),
272
+ cwd: cwd ?? getProjectDir(),
272
273
  });
273
274
  }
274
275
 
@@ -277,7 +278,7 @@ export async function discoverContextFiles(
277
278
  */
278
279
  export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise<PromptTemplate[]> {
279
280
  return await loadPromptTemplatesInternal({
280
- cwd: cwd ?? process.cwd(),
281
+ cwd: cwd ?? getProjectDir(),
281
282
  agentDir: agentDir ?? getDefaultAgentDir(),
282
283
  });
283
284
  }
@@ -286,14 +287,14 @@ export async function discoverPromptTemplates(cwd?: string, agentDir?: string):
286
287
  * Discover file-based slash commands from commands/ directories.
287
288
  */
288
289
  export async function discoverSlashCommands(cwd?: string): Promise<FileSlashCommand[]> {
289
- return loadSlashCommandsInternal({ cwd: cwd ?? process.cwd() });
290
+ return loadSlashCommandsInternal({ cwd: cwd ?? getProjectDir() });
290
291
  }
291
292
 
292
293
  /**
293
294
  * Discover custom commands (TypeScript slash commands) from cwd and agentDir.
294
295
  */
295
296
  export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise<CustomCommandsLoadResult> {
296
- const resolvedCwd = cwd ?? process.cwd();
297
+ const resolvedCwd = cwd ?? getProjectDir();
297
298
  const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
298
299
 
299
300
  return loadCustomCommandsInternal({
@@ -307,7 +308,7 @@ export async function discoverCustomTSCommands(cwd?: string, agentDir?: string):
307
308
  * Returns the manager and loaded tools.
308
309
  */
309
310
  export async function discoverMCPServers(cwd?: string): Promise<MCPToolsLoadResult> {
310
- const resolvedCwd = cwd ?? process.cwd();
311
+ const resolvedCwd = cwd ?? getProjectDir();
311
312
  return discoverAndLoadMCPTools(resolvedCwd);
312
313
  }
313
314
 
@@ -469,7 +470,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
469
470
  * model: myModel,
470
471
  * getApiKey: async () => Bun.env.MY_KEY,
471
472
  * systemPrompt: 'You are helpful.',
472
- * tools: codingTools({ cwd: process.cwd() }),
473
+ * tools: codingTools({ cwd: getProjectDir() }),
473
474
  * skills: [],
474
475
  * sessionManager: SessionManager.inMemory(),
475
476
  * });
@@ -477,7 +478,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
477
478
  */
478
479
  export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
479
480
  debugStartup("sdk:createAgentSession:entry");
480
- const cwd = options.cwd ?? process.cwd();
481
+ const cwd = options.cwd ?? getProjectDir();
481
482
  const agentDir = options.agentDir ?? getDefaultAgentDir();
482
483
  const eventBus = options.eventBus ?? new EventBus();
483
484
 
@@ -1016,6 +1017,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1016
1017
  .map(name => toolRegistry.get(name))
1017
1018
  .filter((tool): tool is AgentTool => tool !== undefined);
1018
1019
 
1020
+ const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "auto";
1021
+ const preferOpenAICodexWebsockets =
1022
+ openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
1023
+
1019
1024
  agent = new Agent({
1020
1025
  initialState: {
1021
1026
  systemPrompt,
@@ -1036,6 +1041,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1036
1041
  thinkingBudgets: settings.getGroup("thinkingBudgets"),
1037
1042
  temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
1038
1043
  kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
1044
+ preferWebsockets: preferOpenAICodexWebsockets,
1039
1045
  getToolContext: tc => toolContextStore.getContext(tc),
1040
1046
  getApiKey: async provider => {
1041
1047
  // Use the provider argument from the in-flight request;
@@ -1087,6 +1093,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1087
1093
  debugStartup("sdk:createAgentSession");
1088
1094
  time("createAgentSession");
1089
1095
 
1096
+ if (model?.api === "openai-codex-responses") {
1097
+ try {
1098
+ debugStartup("sdk:prewarmCodexWebsocket:start");
1099
+ await prewarmOpenAICodexResponses(model, {
1100
+ apiKey: await modelRegistry.getApiKey(model, sessionId),
1101
+ sessionId,
1102
+ preferWebsockets: preferOpenAICodexWebsockets,
1103
+ providerSessionState: session.providerSessionState,
1104
+ });
1105
+ debugStartup("sdk:prewarmCodexWebsocket:done");
1106
+ time("prewarmCodexWebsocket");
1107
+ } catch (error) {
1108
+ logger.debug("Codex websocket prewarm failed", {
1109
+ error: error instanceof Error ? error.message : String(error),
1110
+ provider: model.provider,
1111
+ model: model.id,
1112
+ });
1113
+ }
1114
+ }
1115
+
1090
1116
  // Warm up LSP servers (connects to detected servers)
1091
1117
  let lspServers: CreateAgentSessionResult["lspServers"];
1092
1118
  if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
@@ -21,6 +21,7 @@ import type {
21
21
  ImageContent,
22
22
  Message,
23
23
  Model,
24
+ ProviderSessionState,
24
25
  TextContent,
25
26
  ToolCall,
26
27
  ToolChoice,
@@ -339,6 +340,7 @@ export class AgentSession {
339
340
  #streamingEditCheckedLineCounts = new Map<string, number>();
340
341
  #streamingEditFileCache = new Map<string, string>();
341
342
  #promptInFlight = false;
343
+ #providerSessionState = new Map<string, ProviderSessionState>();
342
344
 
343
345
  constructor(config: AgentSessionConfig) {
344
346
  this.agent = config.agent;
@@ -358,6 +360,7 @@ export class AgentSession {
358
360
  this.#baseSystemPrompt = this.agent.state.systemPrompt;
359
361
  this.#ttsrManager = config.ttsrManager;
360
362
  this.#forceCopilotAgentInitiator = config.forceCopilotAgentInitiator ?? false;
363
+ this.agent.providerSessionState = this.#providerSessionState;
361
364
 
362
365
  // Always subscribe to agent events for internal handling
363
366
  // (session persistence, hooks, auto-compaction, retry logic)
@@ -369,6 +372,11 @@ export class AgentSession {
369
372
  return this.#modelRegistry;
370
373
  }
371
374
 
375
+ /** Provider-scoped mutable state store for transport/session caches. */
376
+ get providerSessionState(): Map<string, ProviderSessionState> {
377
+ return this.#providerSessionState;
378
+ }
379
+
372
380
  /** TTSR manager for time-traveling stream rules */
373
381
  get ttsrManager(): TtsrManager | undefined {
374
382
  return this.#ttsrManager;
@@ -888,6 +896,10 @@ export class AgentSession {
888
896
  async dispose(): Promise<void> {
889
897
  await this.sessionManager.flush();
890
898
  await cleanupSshResources();
899
+ for (const state of this.#providerSessionState.values()) {
900
+ state.close();
901
+ }
902
+ this.#providerSessionState.clear();
891
903
  this.#disconnectFromAgent();
892
904
  this.#eventListeners = [];
893
905
  }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
4
4
  import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
5
5
  import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils";
6
- import { getBlobsDir, getAgentDir as getDefaultAgentDir } from "@oh-my-pi/pi-utils/dirs";
6
+ import { getBlobsDir, getAgentDir as getDefaultAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
7
  import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
8
8
  import {
9
9
  type BashExecutionMessage,
@@ -2153,10 +2153,10 @@ export class SessionManager {
2153
2153
  sessionDir?: string,
2154
2154
  storage: SessionStorage = new FileSessionStorage(),
2155
2155
  ): Promise<SessionManager> {
2156
- // Extract cwd from session header if possible, otherwise use process.cwd()
2156
+ // Extract cwd from session header if possible, otherwise use getProjectDir()
2157
2157
  const entries = await loadEntriesFromFile(filePath, storage);
2158
2158
  const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
2159
- const cwd = header?.cwd ?? process.cwd();
2159
+ const cwd = header?.cwd ?? getProjectDir();
2160
2160
  // If no sessionDir provided, derive from file's parent directory
2161
2161
  const dir = sessionDir ?? path.resolve(filePath, "..");
2162
2162
  const manager = new SessionManager(cwd, dir, true, storage);
@@ -2188,7 +2188,10 @@ export class SessionManager {
2188
2188
  }
2189
2189
 
2190
2190
  /** Create an in-memory session (no file persistence) */
2191
- static inMemory(cwd: string = process.cwd(), storage: SessionStorage = new MemorySessionStorage()): SessionManager {
2191
+ static inMemory(
2192
+ cwd: string = getProjectDir(),
2193
+ storage: SessionStorage = new MemorySessionStorage(),
2194
+ ): SessionManager {
2192
2195
  const manager = new SessionManager(cwd, "", false, storage);
2193
2196
  manager.#initNewSession();
2194
2197
  return manager;
@@ -4,7 +4,7 @@
4
4
  import * as os from "node:os";
5
5
  import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives";
6
6
  import { $env, logger } from "@oh-my-pi/pi-utils";
7
- import { getGpuCachePath } from "@oh-my-pi/pi-utils/dirs";
7
+ import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
8
8
  import { $ } from "bun";
9
9
  import { contextFileCapability } from "./capability/context-file";
10
10
  import { systemPromptCapability } from "./capability/system-prompt";
@@ -50,41 +50,54 @@ async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<Pre
50
50
  * Returns structured git data or null if not in a git repo.
51
51
  */
52
52
  export async function loadGitContext(cwd: string): Promise<GitContext | null> {
53
- const git = (...args: string[]) =>
54
- $`git ${args}`
55
- .cwd(cwd)
56
- .quiet()
57
- .text()
58
- .catch(() => null)
59
- .then(text => text?.trim() ?? null);
53
+ const runGit = async (args: string[], timeoutMs = 1500): Promise<string | null> => {
54
+ const proc = Bun.spawn(["git", ...args], {
55
+ cwd,
56
+ stdout: "pipe",
57
+ stderr: "ignore",
58
+ });
59
+ const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
60
+ const race = await Promise.race([
61
+ proc.exited.then(() => "exited" as const),
62
+ Bun.sleep(timeoutMs).then(() => "timeout" as const),
63
+ ]);
64
+
65
+ if (race === "timeout") {
66
+ proc.kill();
67
+ await stdoutPromise.catch(() => null);
68
+ logger.debug("Git context command timed out", { cwd, args, timeoutMs });
69
+ return null;
70
+ }
71
+
72
+ const exitCode = await proc.exited;
73
+ const stdout = await stdoutPromise.catch(() => "");
74
+ if (exitCode !== 0) return null;
60
75
 
76
+ const trimmed = stdout.trim();
77
+ return trimmed.length > 0 ? trimmed : "";
78
+ };
61
79
  // Check if inside a git repo
62
- const isGitRepo = await git("rev-parse", "--is-inside-work-tree");
80
+ const isGitRepo = await runGit(["rev-parse", "--is-inside-work-tree"]);
63
81
  if (isGitRepo !== "true") return null;
64
-
65
- // Get current branch
66
- const currentBranch = await git("rev-parse", "--abbrev-ref", "HEAD");
82
+ const currentBranch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
67
83
  if (!currentBranch) return null;
68
-
69
- // Detect main branch (check for 'main' first, then 'master')
70
84
  let mainBranch = "main";
71
- const mainExists = await git("rev-parse", "--verify", "main");
85
+ const mainExists = await runGit(["rev-parse", "--verify", "main"]);
72
86
  if (mainExists === null) {
73
- const masterExists = await git("rev-parse", "--verify", "master");
87
+ const masterExists = await runGit(["rev-parse", "--verify", "master"]);
74
88
  if (masterExists !== null) mainBranch = "master";
75
89
  }
76
90
 
77
- // Get git status (porcelain format for parsing)
78
- const status = (await git("status", "--porcelain")) || "(clean)";
79
-
80
- // Get recent commits
81
- const commits = (await git("log", "--oneline", "-5")) || "(no commits)";
91
+ const [status, commits] = await Promise.all([
92
+ runGit(["status", "--porcelain", "--untracked-files=no"], 2000),
93
+ runGit(["log", "--oneline", "-5"]),
94
+ ]);
82
95
  return {
83
96
  isRepo: true,
84
97
  currentBranch,
85
98
  mainBranch,
86
- status,
87
- commits,
99
+ status: status === "" ? "(clean)" : (status ?? "(status unavailable)"),
100
+ commits: commits && commits.length > 0 ? commits : "(no commits)",
88
101
  };
89
102
  }
90
103
 
@@ -349,7 +362,7 @@ export async function resolvePromptInput(input: string | undefined, description:
349
362
  }
350
363
 
351
364
  export interface LoadContextFilesOptions {
352
- /** Working directory to start walking up from. Default: process.cwd() */
365
+ /** Working directory to start walking up from. Default: getProjectDir() */
353
366
  cwd?: string;
354
367
  }
355
368
 
@@ -361,7 +374,7 @@ export interface LoadContextFilesOptions {
361
374
  export async function loadProjectContextFiles(
362
375
  options: LoadContextFilesOptions = {},
363
376
  ): Promise<Array<{ path: string; content: string; depth?: number }>> {
364
- const resolvedCwd = options.cwd ?? process.cwd();
377
+ const resolvedCwd = options.cwd ?? getProjectDir();
365
378
 
366
379
  const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
367
380
 
@@ -391,7 +404,7 @@ export async function loadProjectContextFiles(
391
404
  * Returns combined content from all discovered SYSTEM.md files.
392
405
  */
393
406
  export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
394
- const resolvedCwd = options.cwd ?? process.cwd();
407
+ const resolvedCwd = options.cwd ?? getProjectDir();
395
408
 
396
409
  const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
397
410
 
@@ -420,7 +433,7 @@ export interface BuildSystemPromptOptions {
420
433
  appendSystemPrompt?: string;
421
434
  /** Skills settings for discovery. */
422
435
  skillsSettings?: SkillsSettings;
423
- /** Working directory. Default: process.cwd() */
436
+ /** Working directory. Default: getProjectDir() */
424
437
  cwd?: string;
425
438
  /** Pre-loaded context files (skips discovery if provided). */
426
439
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
@@ -450,7 +463,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
450
463
  preloadedSkills: providedPreloadedSkills,
451
464
  rules,
452
465
  } = options;
453
- const resolvedCwd = cwd ?? process.cwd();
466
+ const resolvedCwd = cwd ?? getProjectDir();
454
467
  const resolvedCustomPrompt = await resolvePromptInput(customPrompt, "system prompt");
455
468
  const resolvedAppendPrompt = await resolvePromptInput(appendSystemPrompt, "append system prompt");
456
469
 
@@ -57,8 +57,8 @@ export function normalizeBashCommand(command: string): NormalizedCommand {
57
57
  normalized = normalized.slice(0, -fullMatch.length);
58
58
  }
59
59
 
60
- // Clean up multiple horizontal spaces (preserve newlines for heredocs/multiline)
61
- normalized = normalized.replace(/[ \t]{2,}/g, " ").trim();
60
+ // Preserve internal whitespace (important for heredocs / indentation-sensitive scripts)
61
+ normalized = normalized.trim();
62
62
 
63
63
  return {
64
64
  command: normalized,
package/src/tools/bash.ts CHANGED
@@ -4,6 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { $env, isEnoent } from "@oh-my-pi/pi-utils";
7
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
8
  import { type Static, Type } from "@sinclair/typebox";
8
9
  import { renderPromptTemplate } from "../config/prompt-templates";
9
10
  import { type BashResult, executeBash } from "../exec/bash-executor";
@@ -205,7 +206,7 @@ interface BashRenderContext {
205
206
  function formatBashCommand(args: BashRenderArgs, _uiTheme: Theme): string {
206
207
  const command = args.command || "…";
207
208
  const prompt = "$";
208
- const cwd = process.cwd();
209
+ const cwd = getProjectDir();
209
210
  let displayWorkdir = args.cwd;
210
211
 
211
212
  if (displayWorkdir) {
@@ -555,13 +555,13 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
555
555
  if (!response.ok) {
556
556
  return {
557
557
  url,
558
- finalUrl: url,
559
- contentType: "unknown",
558
+ finalUrl: response.finalUrl || url,
559
+ contentType: response.contentType || "unknown",
560
560
  method: "failed",
561
561
  content: "",
562
562
  fetchedAt,
563
563
  truncated: false,
564
- notes: ["Failed to fetch URL"],
564
+ notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL"],
565
565
  };
566
566
  }
567
567
 
@@ -4,6 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { ImageContent } from "@oh-my-pi/pi-ai";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
8
  import { type Static, Type } from "@sinclair/typebox";
8
9
  import { renderPromptTemplate } from "../config/prompt-templates";
9
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -836,7 +837,7 @@ export const pythonToolRenderer = {
836
837
  renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
837
838
  const ui = new ToolUIKit(uiTheme);
838
839
  const cells = args.cells ?? [];
839
- const cwd = process.cwd();
840
+ const cwd = getProjectDir();
840
841
  let displayWorkdir = args.cwd;
841
842
 
842
843
  if (displayWorkdir) {