@oh-my-pi/pi-coding-agent 3.32.0 → 3.34.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 (74) hide show
  1. package/CHANGELOG.md +49 -9
  2. package/README.md +12 -0
  3. package/docs/custom-tools.md +1 -1
  4. package/docs/extensions.md +4 -4
  5. package/docs/hooks.md +2 -2
  6. package/docs/sdk.md +4 -8
  7. package/examples/custom-tools/README.md +2 -2
  8. package/examples/extensions/README.md +1 -1
  9. package/examples/extensions/todo.ts +1 -1
  10. package/examples/hooks/custom-compaction.ts +4 -2
  11. package/examples/hooks/handoff.ts +1 -1
  12. package/examples/hooks/qna.ts +1 -1
  13. package/examples/sdk/02-custom-model.ts +1 -1
  14. package/examples/sdk/README.md +1 -1
  15. package/package.json +5 -5
  16. package/src/capability/ssh.ts +42 -0
  17. package/src/cli/file-processor.ts +1 -1
  18. package/src/cli/list-models.ts +1 -1
  19. package/src/core/agent-session.ts +21 -6
  20. package/src/core/auth-storage.ts +1 -1
  21. package/src/core/compaction/branch-summarization.ts +2 -2
  22. package/src/core/compaction/compaction.ts +2 -2
  23. package/src/core/compaction/utils.ts +1 -1
  24. package/src/core/custom-tools/types.ts +1 -1
  25. package/src/core/extensions/runner.ts +1 -1
  26. package/src/core/extensions/types.ts +1 -1
  27. package/src/core/extensions/wrapper.ts +1 -1
  28. package/src/core/file-mentions.ts +147 -5
  29. package/src/core/hooks/runner.ts +2 -2
  30. package/src/core/hooks/types.ts +1 -1
  31. package/src/core/index.ts +11 -0
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +9 -4
  35. package/src/core/sdk.ts +26 -2
  36. package/src/core/session-manager.ts +3 -2
  37. package/src/core/settings-manager.ts +70 -0
  38. package/src/core/ssh/connection-manager.ts +466 -0
  39. package/src/core/ssh/ssh-executor.ts +190 -0
  40. package/src/core/ssh/sshfs-mount.ts +162 -0
  41. package/src/core/ssh-executor.ts +5 -0
  42. package/src/core/system-prompt.ts +424 -1
  43. package/src/core/title-generator.ts +109 -55
  44. package/src/core/tools/index.test.ts +1 -0
  45. package/src/core/tools/index.ts +3 -0
  46. package/src/core/tools/output.ts +37 -2
  47. package/src/core/tools/read.ts +24 -11
  48. package/src/core/tools/renderers.ts +2 -0
  49. package/src/core/tools/ssh.ts +302 -0
  50. package/src/core/tools/task/index.ts +1 -1
  51. package/src/core/tools/task/render.ts +10 -16
  52. package/src/core/tools/task/types.ts +1 -1
  53. package/src/core/tools/task/worker.ts +1 -1
  54. package/src/core/voice.ts +1 -1
  55. package/src/discovery/index.ts +3 -0
  56. package/src/discovery/ssh.ts +162 -0
  57. package/src/main.ts +2 -1
  58. package/src/modes/interactive/components/assistant-message.ts +1 -1
  59. package/src/modes/interactive/components/bash-execution.ts +9 -10
  60. package/src/modes/interactive/components/custom-message.ts +1 -1
  61. package/src/modes/interactive/components/footer.ts +1 -1
  62. package/src/modes/interactive/components/hook-message.ts +1 -1
  63. package/src/modes/interactive/components/model-selector.ts +1 -1
  64. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  65. package/src/modes/interactive/components/status-line.ts +1 -1
  66. package/src/modes/interactive/components/tree-selector.ts +9 -12
  67. package/src/modes/interactive/interactive-mode.ts +5 -2
  68. package/src/modes/interactive/theme/theme.ts +2 -2
  69. package/src/modes/print-mode.ts +1 -1
  70. package/src/modes/rpc/rpc-client.ts +1 -1
  71. package/src/modes/rpc/rpc-types.ts +1 -1
  72. package/src/prompts/system-prompt.md +4 -0
  73. package/src/prompts/tools/ssh.md +74 -0
  74. package/src/utils/image-resize.ts +1 -1
@@ -131,7 +131,7 @@ function renderJsonTreeLines(
131
131
  pushLine(`${prefix}${iconArray} ${header}`);
132
132
  if (val.length === 0) {
133
133
  pushLine(
134
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
134
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
135
135
  "dim",
136
136
  "[]",
137
137
  )}`,
@@ -140,7 +140,7 @@ function renderJsonTreeLines(
140
140
  }
141
141
  if (depth >= maxDepth) {
142
142
  pushLine(
143
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
143
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
144
144
  "dim",
145
145
  theme.format.ellipsis,
146
146
  )}`,
@@ -164,7 +164,7 @@ function renderJsonTreeLines(
164
164
  const entries = Object.entries(val as Record<string, unknown>);
165
165
  if (entries.length === 0) {
166
166
  pushLine(
167
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
167
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
168
168
  "dim",
169
169
  "{}",
170
170
  )}`,
@@ -173,7 +173,7 @@ function renderJsonTreeLines(
173
173
  }
174
174
  if (depth >= maxDepth) {
175
175
  pushLine(
176
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
176
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg(
177
177
  "dim",
178
178
  theme.format.ellipsis,
179
179
  )}`,
@@ -288,10 +288,8 @@ function renderAgentProgress(
288
288
  spinnerFrame?: number,
289
289
  ): string[] {
290
290
  const lines: string[] = [];
291
- const prefix = isLast
292
- ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
293
- : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
294
- const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
291
+ const prefix = isLast ? theme.tree.last : theme.tree.branch;
292
+ const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
295
293
 
296
294
  const icon = getStatusIcon(progress.status, theme, spinnerFrame);
297
295
  const iconColor =
@@ -460,10 +458,8 @@ function renderFindings(
460
458
  for (let i = 0; i < displayCount; i++) {
461
459
  const finding = findings[i];
462
460
  const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
463
- const findingPrefix = isLastFinding
464
- ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
465
- : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
466
- const findingContinue = isLastFinding ? " " : `${theme.boxSharp.vertical} `;
461
+ const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch;
462
+ const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
467
463
 
468
464
  const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
469
465
  const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
@@ -496,10 +492,8 @@ function renderFindings(
496
492
  */
497
493
  function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
498
494
  const lines: string[] = [];
499
- const prefix = isLast
500
- ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
501
- : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
502
- const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
495
+ const prefix = isLast ? theme.tree.last : theme.tree.branch;
496
+ const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
503
497
 
504
498
  const aborted = result.aborted ?? false;
505
499
  const success = !aborted && result.exitCode === 0;
@@ -1,4 +1,4 @@
1
- import type { Usage } from "@mariozechner/pi-ai";
1
+ import type { Usage } from "@oh-my-pi/pi-ai";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
3
 
4
4
  /** Source of an agent definition */
@@ -13,8 +13,8 @@
13
13
  * 5. Parent can send { type: "abort" } to request cancellation
14
14
  */
15
15
 
16
- import type { Api, Model } from "@mariozechner/pi-ai";
17
16
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
18
18
  import type { AgentSessionEvent } from "../../agent-session";
19
19
  import { parseModelPattern, parseModelString } from "../../model-resolver";
20
20
  import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
package/src/core/voice.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { unlinkSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { completeSimple, type Model } from "@mariozechner/pi-ai";
4
+ import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
5
5
  import { nanoid } from "nanoid";
6
6
  import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
7
7
  import { logger } from "./logger";
@@ -18,6 +18,7 @@ import "../capability/settings";
18
18
  import "../capability/skill";
19
19
  import "../capability/slash-command";
20
20
  import "../capability/system-prompt";
21
+ import "../capability/ssh";
21
22
  import "../capability/tool";
22
23
 
23
24
  // Import providers (each registers itself on import)
@@ -32,6 +33,7 @@ import "./github";
32
33
  import "./vscode";
33
34
  import "./agents-md";
34
35
  import "./mcp-json";
36
+ import "./ssh";
35
37
 
36
38
  export type { ContextFile } from "../capability/context-file";
37
39
  export type { Extension, ExtensionManifest } from "../capability/extension";
@@ -70,6 +72,7 @@ export type { Rule, RuleFrontmatter } from "../capability/rule";
70
72
  export type { Settings } from "../capability/settings";
71
73
  export type { Skill, SkillFrontmatter } from "../capability/skill";
72
74
  export type { SlashCommand } from "../capability/slash-command";
75
+ export type { SSHHost } from "../capability/ssh";
73
76
  export type { SystemPrompt } from "../capability/system-prompt";
74
77
  export type { CustomTool } from "../capability/tool";
75
78
  // Re-export types
@@ -0,0 +1,162 @@
1
+ /**
2
+ * SSH JSON Provider
3
+ *
4
+ * Discovers SSH hosts from ssh.json or .ssh.json in the project root.
5
+ * Priority: 5 (low, project-level only)
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { registerProvider } from "../capability/index";
10
+ import { type SSHHost, sshCapability } from "../capability/ssh";
11
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
12
+ import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
13
+
14
+ const PROVIDER_ID = "ssh-json";
15
+ const DISPLAY_NAME = "SSH Config";
16
+
17
+ interface SSHConfigFile {
18
+ hosts?: Record<
19
+ string,
20
+ {
21
+ host?: string;
22
+ username?: string;
23
+ port?: number | string;
24
+ compat?: boolean | string;
25
+ key?: string;
26
+ keyPath?: string;
27
+ description?: string;
28
+ }
29
+ >;
30
+ }
31
+
32
+ function expandTilde(value: string, home: string): string {
33
+ if (value === "~") return home;
34
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
35
+ return `${home}${value.slice(1)}`;
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function parsePort(value: number | string | undefined): number | undefined {
41
+ if (value === undefined) return undefined;
42
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
43
+ const parsed = Number.parseInt(value, 10);
44
+ return Number.isNaN(parsed) ? undefined : parsed;
45
+ }
46
+
47
+ function parseCompat(value: boolean | string | undefined): boolean | undefined {
48
+ if (value === undefined) return undefined;
49
+ if (typeof value === "boolean") return value;
50
+ const normalized = value.trim().toLowerCase();
51
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
52
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
53
+ return undefined;
54
+ }
55
+
56
+ function normalizeHost(
57
+ name: string,
58
+ raw: NonNullable<SSHConfigFile["hosts"]>[string],
59
+ source: SourceMeta,
60
+ home: string,
61
+ warnings: string[],
62
+ ): SSHHost | null {
63
+ if (!raw.host) {
64
+ warnings.push(`Missing host for SSH entry: ${name}`);
65
+ return null;
66
+ }
67
+
68
+ const port = parsePort(raw.port);
69
+ if (raw.port !== undefined && port === undefined) {
70
+ warnings.push(`Invalid port for SSH entry ${name}: ${String(raw.port)}`);
71
+ }
72
+
73
+ const compat = parseCompat(raw.compat);
74
+ if (raw.compat !== undefined && compat === undefined) {
75
+ warnings.push(`Invalid compat flag for SSH entry ${name}: ${String(raw.compat)}`);
76
+ }
77
+
78
+ const keyValue = raw.keyPath ?? raw.key;
79
+ const keyPath = keyValue ? expandTilde(keyValue, home) : undefined;
80
+
81
+ return {
82
+ name,
83
+ host: raw.host,
84
+ username: raw.username,
85
+ port,
86
+ keyPath,
87
+ description: raw.description,
88
+ compat,
89
+ _source: source,
90
+ };
91
+ }
92
+
93
+ function loadSshJsonFile(ctx: LoadContext, path: string): LoadResult<SSHHost> {
94
+ const items: SSHHost[] = [];
95
+ const warnings: string[] = [];
96
+
97
+ if (!ctx.fs.isFile(path)) {
98
+ return { items, warnings };
99
+ }
100
+
101
+ const content = ctx.fs.readFile(path);
102
+ if (content === null) {
103
+ warnings.push(`Failed to read ${path}`);
104
+ return { items, warnings };
105
+ }
106
+
107
+ const parsed = parseJSON<SSHConfigFile>(content);
108
+ if (!parsed) {
109
+ warnings.push(`Failed to parse JSON in ${path}`);
110
+ return { items, warnings };
111
+ }
112
+
113
+ const config = expandEnvVarsDeep(parsed);
114
+ if (!config.hosts || typeof config.hosts !== "object") {
115
+ warnings.push(`Missing hosts in ${path}`);
116
+ return { items, warnings };
117
+ }
118
+
119
+ const source = createSourceMeta(PROVIDER_ID, path, "project");
120
+ for (const [name, rawHost] of Object.entries(config.hosts)) {
121
+ if (!name.trim()) {
122
+ warnings.push(`Invalid SSH host name in ${path}`);
123
+ continue;
124
+ }
125
+ if (!rawHost || typeof rawHost !== "object") {
126
+ warnings.push(`Invalid host entry in ${path}: ${name}`);
127
+ continue;
128
+ }
129
+ const host = normalizeHost(name, rawHost, source, ctx.home, warnings);
130
+ if (host) items.push(host);
131
+ }
132
+
133
+ return {
134
+ items,
135
+ warnings: warnings.length > 0 ? warnings : undefined,
136
+ };
137
+ }
138
+
139
+ function load(ctx: LoadContext): LoadResult<SSHHost> {
140
+ const allItems: SSHHost[] = [];
141
+ const allWarnings: string[] = [];
142
+
143
+ for (const filename of ["ssh.json", ".ssh.json"]) {
144
+ const path = join(ctx.cwd, filename);
145
+ const result = loadSshJsonFile(ctx, path);
146
+ allItems.push(...result.items);
147
+ if (result.warnings) allWarnings.push(...result.warnings);
148
+ }
149
+
150
+ return {
151
+ items: allItems,
152
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
153
+ };
154
+ }
155
+
156
+ registerProvider(sshCapability.id, {
157
+ id: PROVIDER_ID,
158
+ displayName: DISPLAY_NAME,
159
+ description: "Load SSH hosts from ssh.json or .ssh.json in the project root",
160
+ priority: 5,
161
+ load,
162
+ });
package/src/main.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { join, resolve } from "node:path";
10
- import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
10
+ import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
11
11
  import chalk from "chalk";
12
12
  import { type Args, parseArgs, printHelp } from "./cli/args";
13
13
  import { processFileArguments } from "./cli/file-processor";
@@ -421,6 +421,7 @@ export async function main(args: string[]) {
421
421
 
422
422
  const cwd = process.cwd();
423
423
  const settingsManager = SettingsManager.create(cwd);
424
+ settingsManager.applyEnvironmentVariables();
424
425
  time("SettingsManager.create");
425
426
  const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
426
427
  time("prepareInitialMessage");
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@mariozechner/pi-ai";
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { getMarkdownTheme, theme } from "../theme/theme";
4
4
 
@@ -27,12 +27,10 @@ export class BashExecutionComponent extends Container {
27
27
  private fullOutputPath?: string;
28
28
  private expanded = false;
29
29
  private contentContainer: Container;
30
- private ui: TUI;
31
30
 
32
31
  constructor(command: string, ui: TUI, excludeFromContext = false) {
33
32
  super();
34
33
  this.command = command;
35
- this.ui = ui;
36
34
 
37
35
  // Use dim border for excluded-from-context commands (!! prefix)
38
36
  const colorKey = excludeFromContext ? "dim" : "bashMode";
@@ -142,15 +140,16 @@ export class BashExecutionComponent extends Container {
142
140
  const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
143
141
  this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
144
142
  } else {
145
- // Use shared visual truncation utility
143
+ // Use shared visual truncation utility, recomputed per render width
146
144
  const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
147
- const { visualLines } = truncateToVisualLines(
148
- `\n${styledOutput}`,
149
- PREVIEW_LINES,
150
- this.ui.terminal.columns,
151
- 1, // padding
152
- );
153
- this.contentContainer.addChild({ render: () => visualLines, invalidate: () => {} });
145
+ const previewText = `\n${styledOutput}`;
146
+ this.contentContainer.addChild({
147
+ render: (width: number) => {
148
+ const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
149
+ return visualLines;
150
+ },
151
+ invalidate: () => {},
152
+ });
154
153
  }
155
154
  }
156
155
 
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@mariozechner/pi-ai";
1
+ import type { TextContent } from "@oh-my-pi/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { MessageRenderer } from "../../../core/extensions/types";
@@ -1,5 +1,5 @@
1
1
  import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
2
- import type { AssistantMessage } from "@mariozechner/pi-ai";
2
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
3
3
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
4
4
  import { dirname, join } from "path";
5
5
  import type { AgentSession } from "../../../core/agent-session";
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@mariozechner/pi-ai";
1
+ import type { TextContent } from "@oh-my-pi/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { HookMessageRenderer } from "../../../core/hooks/types";
@@ -1,4 +1,4 @@
1
- import { type Model, modelsAreEqual } from "@mariozechner/pi-ai";
1
+ import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
2
  import {
3
3
  Container,
4
4
  Input,
@@ -1,4 +1,4 @@
1
- import { getOAuthProviders, type OAuthProviderInfo } from "@mariozechner/pi-ai";
1
+ import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
2
2
  import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
3
  import type { AuthStorage } from "../../../core/auth-storage";
4
4
  import { theme } from "../theme/theme";
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@mariozechner/pi-ai";
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
3
3
  import { type FSWatcher, watch } from "fs";
4
4
  import { dirname, join } from "path";
@@ -437,13 +437,10 @@ class TreeList implements Component {
437
437
 
438
438
  // Build prefix with gutters at their correct positions
439
439
  // Each gutter has a position (displayIndent where its connector was shown)
440
- const connector =
441
- flatNode.showConnector && !flatNode.isVirtualRootChild
442
- ? flatNode.isLast
443
- ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal} `
444
- : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal} `
445
- : "";
446
- const connectorPosition = connector ? displayIndent - 1 : -1;
440
+ const hasConnector = flatNode.showConnector && !flatNode.isVirtualRootChild;
441
+ const connectorSymbol = hasConnector ? (flatNode.isLast ? theme.tree.last : theme.tree.branch) : "";
442
+ const connectorChars = hasConnector ? Array.from(connectorSymbol) : [];
443
+ const connectorPosition = hasConnector ? displayIndent - 1 : -1;
447
444
 
448
445
  // Build prefix char by char, placing gutters and connector at their positions
449
446
  const totalChars = displayIndent * 3;
@@ -456,18 +453,18 @@ class TreeList implements Component {
456
453
  const gutter = flatNode.gutters.find((g) => g.position === level);
457
454
  if (gutter) {
458
455
  if (posInLevel === 0) {
459
- prefixChars.push(gutter.show ? theme.boxSharp.vertical : " ");
456
+ prefixChars.push(gutter.show ? theme.tree.vertical : " ");
460
457
  } else {
461
458
  prefixChars.push(" ");
462
459
  }
463
- } else if (connector && level === connectorPosition) {
460
+ } else if (hasConnector && level === connectorPosition) {
464
461
  // Connector at this level
465
462
  if (posInLevel === 0) {
466
- prefixChars.push(flatNode.isLast ? theme.boxSharp.bottomLeft : theme.boxSharp.teeRight);
463
+ prefixChars.push(connectorChars[0] ?? " ");
467
464
  } else if (posInLevel === 1) {
468
- prefixChars.push(theme.boxSharp.horizontal);
465
+ prefixChars.push(connectorChars[1] ?? theme.tree.horizontal);
469
466
  } else {
470
- prefixChars.push(" ");
467
+ prefixChars.push(connectorChars[2] ?? " ");
471
468
  }
472
469
  } else {
473
470
  prefixChars.push(" ");
@@ -6,8 +6,8 @@
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@mariozechner/pi-ai";
10
9
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
10
+ import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
11
11
  import type { SlashCommand } from "@oh-my-pi/pi-tui";
12
12
  import {
13
13
  CombinedAutocompleteProvider,
@@ -1215,6 +1215,9 @@ export class InteractiveMode {
1215
1215
  this.editor.setText("");
1216
1216
  this.updatePendingMessagesDisplay();
1217
1217
  this.ui.requestRender();
1218
+ } else if (event.message.role === "fileMention") {
1219
+ this.addMessageToChat(event.message);
1220
+ this.ui.requestRender();
1218
1221
  } else if (event.message.role === "assistant") {
1219
1222
  this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock);
1220
1223
  this.streamingMessage = event.message;
@@ -1566,7 +1569,7 @@ export class InteractiveMode {
1566
1569
  case "fileMention": {
1567
1570
  // Render compact file mention display
1568
1571
  for (const file of message.files) {
1569
- const text = `${theme.fg("dim", `${theme.tree.hook} `)}${theme.fg("muted", "Read")} ${theme.fg(
1572
+ const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
1570
1573
  "accent",
1571
1574
  file.path,
1572
1575
  )} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
@@ -209,8 +209,8 @@ const UNICODE_SYMBOLS: SymbolMap = {
209
209
  "tree.vertical": "│",
210
210
  // pick: ─ | alt: ━ ═ ╌ ┄
211
211
  "tree.horizontal": "─",
212
- // pick: | alt: ╰ ↳
213
- "tree.hook": "",
212
+ // pick: | alt: ╰ ↳
213
+ "tree.hook": "\u2514",
214
214
  // Box Drawing - Rounded
215
215
  // pick: ╭ | alt: ┌ ┏ ╔
216
216
  "boxRound.topLeft": "╭",
@@ -6,7 +6,7 @@
6
6
  * - `omp --mode json "prompt"` - JSON event stream
7
7
  */
8
8
 
9
- import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
9
+ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
10
  import type { AgentSession } from "../core/agent-session";
11
11
 
12
12
  /**
@@ -4,8 +4,8 @@
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
6
 
7
- import type { ImageContent } from "@mariozechner/pi-ai";
8
7
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import type { Subprocess } from "bun";
10
10
  import type { SessionStats } from "../../core/agent-session";
11
11
  import type { BashResult } from "../../core/bash-executor";
@@ -5,8 +5,8 @@
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
7
 
8
- import type { ImageContent, Model } from "@mariozechner/pi-ai";
9
8
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
+ import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
10
10
  import type { SessionStats } from "../../core/agent-session";
11
11
  import type { BashResult } from "../../core/bash-executor";
12
12
  import type { CompactionResult } from "../../core/compaction/index";
@@ -21,6 +21,10 @@ Core behavior:
21
21
  - After each tool result, check relevance; iterate or clarify if results conflict or are insufficient.
22
22
  - Use concise, scannable responses; include file paths in backticks; use short bullets for multi-item lists; avoid dumping large files.
23
23
 
24
+ <environment>
25
+ {{environmentInfo}}
26
+ </environment>
27
+
24
28
  Documentation:
25
29
  - Main documentation: {{readmePath}}
26
30
  - Additional docs: {{docsPath}}
@@ -0,0 +1,74 @@
1
+ Execute commands on remote SSH hosts.
2
+
3
+ ## Critical: Match Commands to Host Shell
4
+
5
+ Each host runs a specific shell. **You MUST use commands native to that shell.**
6
+
7
+ ### Command Reference
8
+
9
+ **linux/bash, linux/zsh, macos/bash, macos/zsh** — Unix-like systems:
10
+ - Files: `ls`, `cat`, `head`, `tail`, `grep`, `find`
11
+ - System: `ps`, `top`, `df`, `uname`, `free` (Linux), `df`, `uname`, `top` (macOS)
12
+ - Navigation: `cd`, `pwd`
13
+
14
+ **windows/bash, windows/sh** — Windows with Unix compatibility layer (WSL, Cygwin, Git Bash):
15
+ - Files: `ls`, `cat`, `head`, `tail`, `grep`, `find`
16
+ - System: `ps`, `top`, `df`, `uname`
17
+ - Navigation: `cd`, `pwd`
18
+ - Note: These are Windows hosts but use Unix commands
19
+
20
+ **windows/powershell** — Native Windows PowerShell:
21
+ - Files: `Get-ChildItem`, `Get-Content`, `Select-String`
22
+ - System: `Get-Process`, `Get-ComputerInfo`
23
+ - Navigation: `Set-Location`, `Get-Location`
24
+
25
+ **windows/cmd** — Native Windows Command Prompt:
26
+ - Files: `dir`, `type`, `findstr`, `where`
27
+ - System: `tasklist`, `systeminfo`
28
+ - Navigation: `cd`, `echo %CD%`
29
+
30
+ ## Execution Pattern
31
+
32
+ 1. Check the host's shell type from "Available hosts" below
33
+ 2. Use ONLY commands for that shell type
34
+ 3. Construct your command using the reference above
35
+
36
+ ## Examples
37
+
38
+ <example>
39
+ Task: List files in /home/user on host "server1"
40
+ Host: server1 (10.0.0.1) | linux/bash
41
+ Command: `ls -la /home/user`
42
+ </example>
43
+
44
+ <example>
45
+ Task: Show running processes on host "winbox"
46
+ Host: winbox (192.168.1.5) | windows/cmd
47
+ Command: `tasklist /v`
48
+ </example>
49
+
50
+ <example>
51
+ Task: Check disk usage on host "wsl-dev"
52
+ Host: wsl-dev (192.168.1.10) | windows/bash
53
+ Command: `df -h`
54
+ Note: Windows host with WSL — use Unix commands
55
+ </example>
56
+
57
+ <example>
58
+ Task: Get system info on host "macbook"
59
+ Host: macbook (10.0.0.20) | macos/zsh
60
+ Command: `uname -a && sw_vers`
61
+ </example>
62
+
63
+ ## Parameters
64
+
65
+ - **host**: Host name from "Available hosts" below
66
+ - **command**: Command to execute (see Command Reference above)
67
+ - **cwd**: Working directory (optional)
68
+ - **timeout**: Timeout in seconds (optional)
69
+
70
+ ## Output
71
+
72
+ Truncated at 50KB. Exit codes captured.
73
+
74
+ **Before executing: verify host shell type below and use matching commands.**
@@ -1,4 +1,4 @@
1
- import type { ImageContent } from "@mariozechner/pi-ai";
1
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
2
  import { getImageDimensionsWithImageMagick, resizeWithImageMagick } from "./image-magick.js";
3
3
 
4
4
  export interface ImageResizeOptions {