@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.1

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 (70) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +217 -51
  9. package/src/core/auth-storage.ts +456 -47
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +4 -4
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -3,89 +3,103 @@
3
3
  * Displays a list of string options with keyboard navigation.
4
4
  */
5
5
 
6
- import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, Text } from "@oh-my-pi/pi-tui";
6
+ import {
7
+ Container,
8
+ isArrowDown,
9
+ isArrowUp,
10
+ isCtrlC,
11
+ isEnter,
12
+ isEscape,
13
+ Spacer,
14
+ Text,
15
+ type TUI,
16
+ } from "@oh-my-pi/pi-tui";
7
17
  import { theme } from "../theme/theme";
18
+ import { CountdownTimer } from "./countdown-timer";
8
19
  import { DynamicBorder } from "./dynamic-border";
9
20
 
21
+ export interface HookSelectorOptions {
22
+ tui?: TUI;
23
+ timeout?: number;
24
+ }
25
+
10
26
  export class HookSelectorComponent extends Container {
11
27
  private options: string[];
12
28
  private selectedIndex = 0;
13
29
  private listContainer: Container;
14
30
  private onSelectCallback: (option: string) => void;
15
31
  private onCancelCallback: () => void;
16
-
17
- constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) {
32
+ private titleText: Text;
33
+ private baseTitle: string;
34
+ private countdown: CountdownTimer | undefined;
35
+
36
+ constructor(
37
+ title: string,
38
+ options: string[],
39
+ onSelect: (option: string) => void,
40
+ onCancel: () => void,
41
+ opts?: HookSelectorOptions,
42
+ ) {
18
43
  super();
19
44
 
20
45
  this.options = options;
21
46
  this.onSelectCallback = onSelect;
22
47
  this.onCancelCallback = onCancel;
48
+ this.baseTitle = title;
23
49
 
24
- // Add top border
25
50
  this.addChild(new DynamicBorder());
26
51
  this.addChild(new Spacer(1));
27
52
 
28
- // Add title
29
- this.addChild(new Text(theme.fg("accent", title), 1, 0));
53
+ this.titleText = new Text(theme.fg("accent", title), 1, 0);
54
+ this.addChild(this.titleText);
30
55
  this.addChild(new Spacer(1));
31
56
 
32
- // Create list container
57
+ if (opts?.timeout && opts.timeout > 0 && opts.tui) {
58
+ this.countdown = new CountdownTimer(
59
+ opts.timeout,
60
+ opts.tui,
61
+ (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
62
+ () => this.onCancelCallback(),
63
+ );
64
+ }
65
+
33
66
  this.listContainer = new Container();
34
67
  this.addChild(this.listContainer);
35
-
36
68
  this.addChild(new Spacer(1));
37
-
38
- // Add hint
39
- this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0));
40
-
69
+ this.addChild(new Text(theme.fg("dim", "up/down navigate enter select esc cancel"), 1, 0));
41
70
  this.addChild(new Spacer(1));
42
-
43
- // Add bottom border
44
71
  this.addChild(new DynamicBorder());
45
72
 
46
- // Initial render
47
73
  this.updateList();
48
74
  }
49
75
 
50
76
  private updateList(): void {
51
77
  this.listContainer.clear();
52
-
53
78
  for (let i = 0; i < this.options.length; i++) {
54
- const option = this.options[i];
55
79
  const isSelected = i === this.selectedIndex;
56
-
57
- let text = "";
58
- if (isSelected) {
59
- text = theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", option);
60
- } else {
61
- text = ` ${theme.fg("text", option)}`;
62
- }
63
-
80
+ const text = isSelected
81
+ ? theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", this.options[i])
82
+ : ` ${theme.fg("text", this.options[i])}`;
64
83
  this.listContainer.addChild(new Text(text, 1, 0));
65
84
  }
66
85
  }
67
86
 
68
87
  handleInput(keyData: string): void {
69
- // Up arrow or k
70
88
  if (isArrowUp(keyData) || keyData === "k") {
71
89
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
72
90
  this.updateList();
73
- }
74
- // Down arrow or j
75
- else if (isArrowDown(keyData) || keyData === "j") {
91
+ } else if (isArrowDown(keyData) || keyData === "j") {
76
92
  this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
77
93
  this.updateList();
78
- }
79
- // Enter
80
- else if (isEnter(keyData) || keyData === "\n") {
94
+ } else if (isEnter(keyData) || keyData === "\n") {
81
95
  const selected = this.options[this.selectedIndex];
82
- if (selected) {
83
- this.onSelectCallback(selected);
84
- }
85
- }
86
- // Escape or Ctrl+C
87
- else if (isEscape(keyData) || isCtrlC(keyData)) {
96
+ if (selected) this.onSelectCallback(selected);
97
+ } else if (isEscape(keyData) || isCtrlC(keyData)) {
88
98
  this.onCancelCallback();
89
99
  }
90
100
  }
101
+
102
+ dispose(): void {
103
+ this.countdown?.dispose();
104
+ }
91
105
  }
@@ -0,0 +1,39 @@
1
+ // UI Components barrel export
2
+ export { ArminComponent } from "./armin";
3
+ export { AssistantMessageComponent } from "./assistant-message";
4
+ export { BashExecutionComponent } from "./bash-execution";
5
+ export { BorderedLoader } from "./bordered-loader";
6
+ export { BranchSummaryMessageComponent } from "./branch-summary-message";
7
+ export { CompactionSummaryMessageComponent } from "./compaction-summary-message";
8
+ export { CountdownTimer } from "./countdown-timer";
9
+ export { CustomEditor } from "./custom-editor";
10
+ export { CustomMessageComponent } from "./custom-message";
11
+ export { type RenderDiffOptions, renderDiff } from "./diff";
12
+ export { DynamicBorder } from "./dynamic-border";
13
+ export { FooterComponent } from "./footer";
14
+ export { HookEditorComponent } from "./hook-editor";
15
+ export { HookInputComponent, type HookInputOptions } from "./hook-input";
16
+ export { HookMessageComponent } from "./hook-message";
17
+ export { HookSelectorComponent } from "./hook-selector";
18
+ export { LoginDialogComponent } from "./login-dialog";
19
+ export { ModelSelectorComponent } from "./model-selector";
20
+ export { OAuthSelectorComponent } from "./oauth-selector";
21
+ export { QueueModeSelectorComponent } from "./queue-mode-selector";
22
+ export { SessionSelectorComponent } from "./session-selector";
23
+ export {
24
+ type SettingChangeHandler,
25
+ type SettingsCallbacks,
26
+ type SettingsRuntimeContext,
27
+ SettingsSelectorComponent,
28
+ } from "./settings-selector";
29
+ export { ShowImagesSelectorComponent } from "./show-images-selector";
30
+ export { StatusLineComponent } from "./status-line";
31
+ export { ThemeSelectorComponent } from "./theme-selector";
32
+ export { ThinkingSelectorComponent } from "./thinking-selector";
33
+ export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution";
34
+ export { TreeSelectorComponent } from "./tree-selector";
35
+ export { TtsrNotificationComponent } from "./ttsr-notification";
36
+ export { UserMessageComponent } from "./user-message";
37
+ export { UserMessageSelectorComponent } from "./user-message-selector";
38
+ export { truncateToVisualLines, type VisualTruncateResult } from "./visual-truncate";
39
+ export { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome";
@@ -0,0 +1,160 @@
1
+ import { getOAuthProviders } from "@oh-my-pi/pi-ai";
2
+ import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
3
+ import { theme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ /**
7
+ * Login dialog component - replaces editor during OAuth login flow
8
+ */
9
+ export class LoginDialogComponent extends Container {
10
+ private contentContainer: Container;
11
+ private input: Input;
12
+ private tui: TUI;
13
+ private abortController = new AbortController();
14
+ private inputResolver?: (value: string) => void;
15
+ private inputRejecter?: (error: Error) => void;
16
+
17
+ constructor(
18
+ tui: TUI,
19
+ providerId: string,
20
+ private onComplete: (success: boolean, message?: string) => void,
21
+ ) {
22
+ super();
23
+ this.tui = tui;
24
+
25
+ const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
26
+ const providerName = providerInfo?.name || providerId;
27
+
28
+ // Top border
29
+ this.addChild(new DynamicBorder());
30
+
31
+ // Title
32
+ this.addChild(new Text(theme.fg("warning", `Login to ${providerName}`), 1, 0));
33
+
34
+ // Dynamic content area
35
+ this.contentContainer = new Container();
36
+ this.addChild(this.contentContainer);
37
+
38
+ // Input (always present, used when needed)
39
+ this.input = new Input();
40
+ this.input.onSubmit = () => {
41
+ if (this.inputResolver) {
42
+ this.inputResolver(this.input.getValue());
43
+ this.inputResolver = undefined;
44
+ this.inputRejecter = undefined;
45
+ }
46
+ };
47
+ this.input.onEscape = () => {
48
+ this.cancel();
49
+ };
50
+
51
+ // Bottom border
52
+ this.addChild(new DynamicBorder());
53
+ }
54
+
55
+ get signal(): AbortSignal {
56
+ return this.abortController.signal;
57
+ }
58
+
59
+ private cancel(): void {
60
+ this.abortController.abort();
61
+ if (this.inputRejecter) {
62
+ this.inputRejecter(new Error("Login cancelled"));
63
+ this.inputResolver = undefined;
64
+ this.inputRejecter = undefined;
65
+ }
66
+ this.onComplete(false, "Login cancelled");
67
+ }
68
+
69
+ /**
70
+ * Called by onAuth callback - show URL and optional instructions
71
+ */
72
+ showAuth(url: string, instructions?: string): void {
73
+ this.contentContainer.clear();
74
+ this.contentContainer.addChild(new Spacer(1));
75
+ this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
76
+
77
+ const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
78
+ const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
79
+ this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
80
+
81
+ if (instructions) {
82
+ this.contentContainer.addChild(new Spacer(1));
83
+ this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
84
+ }
85
+
86
+ // Try to open browser using Bun.spawn
87
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
88
+ Bun.spawn([openCmd, url], { stdout: "ignore", stderr: "ignore" });
89
+
90
+ this.tui.requestRender();
91
+ }
92
+
93
+ /**
94
+ * Show input for manual code/URL entry (for callback server providers)
95
+ */
96
+ showManualInput(prompt: string): Promise<string> {
97
+ this.contentContainer.addChild(new Spacer(1));
98
+ this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
99
+ this.contentContainer.addChild(this.input);
100
+ this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0));
101
+ this.tui.requestRender();
102
+
103
+ return new Promise((resolve, reject) => {
104
+ this.inputResolver = resolve;
105
+ this.inputRejecter = reject;
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Called by onPrompt callback - show prompt and wait for input
111
+ * Note: Does NOT clear content, appends to existing (preserves URL from showAuth)
112
+ */
113
+ showPrompt(message: string, placeholder?: string): Promise<string> {
114
+ this.contentContainer.addChild(new Spacer(1));
115
+ this.contentContainer.addChild(new Text(theme.fg("text", message), 1, 0));
116
+ if (placeholder) {
117
+ this.contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0));
118
+ }
119
+ this.contentContainer.addChild(this.input);
120
+ this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel, Enter to submit)"), 1, 0));
121
+
122
+ this.input.setValue("");
123
+ this.tui.requestRender();
124
+
125
+ return new Promise((resolve, reject) => {
126
+ this.inputResolver = resolve;
127
+ this.inputRejecter = reject;
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Show waiting message (for polling flows like GitHub Copilot)
133
+ */
134
+ showWaiting(message: string): void {
135
+ this.contentContainer.addChild(new Spacer(1));
136
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
137
+ this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0));
138
+ this.tui.requestRender();
139
+ }
140
+
141
+ /**
142
+ * Called by onProgress callback
143
+ */
144
+ showProgress(message: string): void {
145
+ this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
146
+ this.tui.requestRender();
147
+ }
148
+
149
+ handleInput(data: string): void {
150
+ const kb = getEditorKeybindings();
151
+
152
+ if (kb.matches(data, "selectCancel")) {
153
+ this.cancel();
154
+ return;
155
+ }
156
+
157
+ // Pass to input
158
+ this.input.handleInput(data);
159
+ }
160
+ }
@@ -93,7 +93,7 @@ export class ModelSelectorComponent extends Container {
93
93
  scopedModels: ReadonlyArray<ScopedModelItem>,
94
94
  onSelect: (model: Model<any>, role: string) => void,
95
95
  onCancel: () => void,
96
- options?: { temporaryOnly?: boolean },
96
+ options?: { temporaryOnly?: boolean; initialSearchInput?: string },
97
97
  ) {
98
98
  super();
99
99
 
@@ -105,6 +105,7 @@ export class ModelSelectorComponent extends Container {
105
105
  this.onSelectCallback = onSelect;
106
106
  this.onCancelCallback = onCancel;
107
107
  this.temporaryOnly = options?.temporaryOnly ?? false;
108
+ const initialSearchInput = options?.initialSearchInput;
108
109
 
109
110
  // Load current role assignments from settings
110
111
  this._loadRoleModels();
@@ -129,6 +130,9 @@ export class ModelSelectorComponent extends Container {
129
130
 
130
131
  // Create search input
131
132
  this.searchInput = new Input();
133
+ if (initialSearchInput) {
134
+ this.searchInput.setValue(initialSearchInput);
135
+ }
132
136
  this.searchInput.onSubmit = () => {
133
137
  // Enter on search input opens menu if we have a selection
134
138
  if (this.filteredModels[this.selectedIndex]) {
@@ -156,7 +160,11 @@ export class ModelSelectorComponent extends Container {
156
160
  this.loadModels().then(() => {
157
161
  this.buildProviderTabs();
158
162
  this.updateTabBar();
159
- this.updateList();
163
+ if (initialSearchInput) {
164
+ this.filterModels(initialSearchInput);
165
+ } else {
166
+ this.updateList();
167
+ }
160
168
  // Request re-render after models are loaded
161
169
  this.tui.requestRender();
162
170
  });
@@ -47,7 +47,11 @@ class SessionList implements Component {
47
47
  }
48
48
 
49
49
  private filterSessions(query: string): void {
50
- this.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => session.allMessagesText);
50
+ this.filteredSessions = fuzzyFilter(
51
+ this.allSessions,
52
+ query,
53
+ (session) => `${session.id} ${session.allMessagesText}`,
54
+ );
51
55
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
52
56
  }
53
57
 
@@ -135,6 +135,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
135
135
  get: (sm) => sm.getImageAutoResize(),
136
136
  set: (sm, v) => sm.setImageAutoResize(v),
137
137
  },
138
+ {
139
+ id: "blockImages",
140
+ tab: "config",
141
+ type: "boolean",
142
+ label: "Block images",
143
+ description: "Prevent images from being sent to LLM providers",
144
+ get: (sm) => sm.getBlockImages(),
145
+ set: (sm, v) => sm.setBlockImages(v),
146
+ },
138
147
  {
139
148
  id: "steeringMode",
140
149
  tab: "config",
@@ -1,4 +1,5 @@
1
1
  import { hostname as osHostname } from "node:os";
2
+ import { shortenPath } from "../../../../core/tools/render-utils";
2
3
  import { theme } from "../../theme/theme";
3
4
  import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
4
5
 
@@ -76,10 +77,9 @@ const pathSegment: StatusLineSegment = {
76
77
  const opts = ctx.options.path ?? {};
77
78
 
78
79
  let pwd = process.cwd();
79
- const home = process.env.HOME || process.env.USERPROFILE;
80
80
 
81
- if (opts.abbreviate !== false && home && pwd.startsWith(home)) {
82
- pwd = `~${pwd.slice(home.length)}`;
81
+ if (opts.abbreviate !== false) {
82
+ pwd = shortenPath(pwd);
83
83
  }
84
84
  if (opts.stripWorkPrefix !== false && pwd.startsWith("/work/")) {
85
85
  pwd = pwd.slice(6);
@@ -17,7 +17,6 @@ import { convertToPng } from "../../../utils/image-convert";
17
17
  import { sanitizeBinaryOutput } from "../../../utils/shell";
18
18
  import { theme } from "../theme/theme";
19
19
  import { renderDiff } from "./diff";
20
- import { truncateToVisualLines } from "./visual-truncate";
21
20
 
22
21
  // Preview line limit for bash when not expanded
23
22
  const BASH_PREVIEW_LINES = 5;
@@ -297,6 +296,11 @@ export class ToolExecutionComponent extends Container {
297
296
  this.updateDisplay();
298
297
  }
299
298
 
299
+ override invalidate(): void {
300
+ super.invalidate();
301
+ this.updateDisplay();
302
+ }
303
+
300
304
  private updateDisplay(): void {
301
305
  // Set background based on state
302
306
  const bgFn = this.isPartial
@@ -477,22 +481,11 @@ export class ToolExecutionComponent extends Container {
477
481
  const context: Record<string, unknown> = {};
478
482
 
479
483
  if (this.toolName === "bash" && this.result) {
480
- // Bash needs visual line truncation context
484
+ // Pass raw output and expanded state - renderer handles width-aware truncation
481
485
  const output = this.getTextOutput().trim();
482
- if (output && !this.expanded) {
483
- const styledOutput = output
484
- .split("\n")
485
- .map((line) => theme.fg("toolOutput", line))
486
- .join("\n");
487
- const { visualLines, skippedCount } = truncateToVisualLines(
488
- `\n${styledOutput}`,
489
- BASH_PREVIEW_LINES,
490
- this.ui.terminal.columns - 2,
491
- );
492
- context.visualLines = visualLines;
493
- context.skippedCount = skippedCount;
494
- context.totalVisualLines = skippedCount + visualLines.length;
495
- }
486
+ context.output = output;
487
+ context.expanded = this.expanded;
488
+ context.previewLines = BASH_PREVIEW_LINES;
496
489
  } else if (this.toolName === "edit") {
497
490
  // Edit needs diff preview and renderDiff function
498
491
  context.editDiffPreview = this.editDiffPreview;
@@ -18,6 +18,7 @@ import {
18
18
  truncateToWidth,
19
19
  } from "@oh-my-pi/pi-tui";
20
20
  import type { SessionTreeNode } from "../../../core/session-manager";
21
+ import { shortenPath } from "../../../core/tools/render-utils";
21
22
  import { theme } from "../theme/theme";
22
23
  import { DynamicBorder } from "./dynamic-border";
23
24
 
@@ -607,12 +608,6 @@ class TreeList implements Component {
607
608
  }
608
609
 
609
610
  private formatToolCall(name: string, args: Record<string, unknown>): string {
610
- const shortenPath = (p: string): string => {
611
- const home = process.env.HOME || process.env.USERPROFILE || "";
612
- if (home && p.startsWith(home)) return `~${p.slice(home.length)}`;
613
- return p;
614
- };
615
-
616
611
  switch (name) {
617
612
  case "read": {
618
613
  const path = shortenPath(String(args.path || args.file_path || ""));