@oh-my-pi/pi-coding-agent 11.2.2 → 11.3.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 (95) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/hooks/status-line.ts +1 -1
  5. package/examples/sdk/11-sessions.ts +1 -1
  6. package/package.json +10 -10
  7. package/src/cli/args.ts +9 -6
  8. package/src/cli/update-cli.ts +2 -2
  9. package/src/commands/index/index.ts +2 -5
  10. package/src/commit/agentic/agent.ts +1 -1
  11. package/src/commit/changelog/index.ts +2 -2
  12. package/src/config/keybindings.ts +16 -1
  13. package/src/config/model-registry.ts +25 -20
  14. package/src/config/model-resolver.ts +8 -8
  15. package/src/config/resolve-config-value.ts +92 -0
  16. package/src/config/settings-schema.ts +9 -0
  17. package/src/config.ts +14 -1
  18. package/src/export/html/template.css +7 -0
  19. package/src/export/html/template.generated.ts +1 -1
  20. package/src/export/html/template.js +33 -16
  21. package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
  22. package/src/extensibility/extensions/index.ts +18 -0
  23. package/src/extensibility/extensions/loader.ts +15 -0
  24. package/src/extensibility/extensions/runner.ts +78 -1
  25. package/src/extensibility/extensions/types.ts +131 -5
  26. package/src/extensibility/extensions/wrapper.ts +1 -1
  27. package/src/extensibility/plugins/git-url.ts +270 -0
  28. package/src/extensibility/plugins/index.ts +2 -0
  29. package/src/extensibility/slash-commands.ts +45 -0
  30. package/src/index.ts +7 -0
  31. package/src/lsp/render.ts +50 -43
  32. package/src/lsp/utils.ts +2 -2
  33. package/src/main.ts +11 -10
  34. package/src/mcp/transports/stdio.ts +9 -35
  35. package/src/modes/components/custom-message.ts +0 -8
  36. package/src/modes/components/diff.ts +1 -7
  37. package/src/modes/components/footer.ts +4 -4
  38. package/src/modes/components/model-selector.ts +4 -0
  39. package/src/modes/components/todo-display.ts +13 -3
  40. package/src/modes/components/tool-execution.ts +30 -16
  41. package/src/modes/components/tree-selector.ts +50 -19
  42. package/src/modes/controllers/event-controller.ts +1 -0
  43. package/src/modes/controllers/extension-ui-controller.ts +34 -2
  44. package/src/modes/controllers/input-controller.ts +47 -33
  45. package/src/modes/controllers/selector-controller.ts +10 -15
  46. package/src/modes/interactive-mode.ts +50 -38
  47. package/src/modes/print-mode.ts +6 -0
  48. package/src/modes/rpc/rpc-client.ts +25 -40
  49. package/src/modes/rpc/rpc-mode.ts +36 -31
  50. package/src/modes/rpc/rpc-types.ts +2 -2
  51. package/src/modes/types.ts +1 -0
  52. package/src/modes/utils/ui-helpers.ts +3 -1
  53. package/src/patch/applicator.ts +2 -3
  54. package/src/patch/fuzzy.ts +1 -1
  55. package/src/patch/shared.ts +74 -61
  56. package/src/prompts/system/system-prompt.md +1 -0
  57. package/src/prompts/tools/task.md +6 -0
  58. package/src/sdk.ts +15 -11
  59. package/src/session/agent-session.ts +72 -23
  60. package/src/session/auth-storage.ts +2 -1
  61. package/src/session/blob-store.ts +105 -0
  62. package/src/session/session-manager.ts +112 -75
  63. package/src/session/session-storage.ts +1 -1
  64. package/src/task/executor.ts +19 -9
  65. package/src/task/render.ts +80 -58
  66. package/src/tools/ask.ts +28 -5
  67. package/src/tools/bash.ts +47 -39
  68. package/src/tools/browser.ts +248 -26
  69. package/src/tools/calculator.ts +42 -23
  70. package/src/tools/fetch.ts +33 -16
  71. package/src/tools/find.ts +57 -22
  72. package/src/tools/grep.ts +54 -25
  73. package/src/tools/index.ts +5 -5
  74. package/src/tools/notebook.ts +19 -6
  75. package/src/tools/path-utils.ts +26 -1
  76. package/src/tools/python.ts +20 -14
  77. package/src/tools/read.ts +22 -9
  78. package/src/tools/render-utils.ts +5 -45
  79. package/src/tools/ssh.ts +59 -53
  80. package/src/tools/submit-result.ts +2 -2
  81. package/src/tools/todo-write.ts +32 -14
  82. package/src/tools/truncate.ts +1 -1
  83. package/src/tools/write.ts +39 -24
  84. package/src/tui/output-block.ts +61 -3
  85. package/src/tui/tree-list.ts +4 -4
  86. package/src/tui/utils.ts +71 -1
  87. package/src/utils/frontmatter.ts +1 -1
  88. package/src/utils/mime.ts +1 -1
  89. package/src/utils/title-generator.ts +1 -1
  90. package/src/utils/tools-manager.ts +18 -2
  91. package/src/web/scrapers/osv.ts +4 -1
  92. package/src/web/scrapers/utils.ts +8 -6
  93. package/src/web/scrapers/youtube.ts +1 -1
  94. package/src/web/search/index.ts +1 -1
  95. package/src/web/search/render.ts +96 -90
package/CHANGELOG.md CHANGED
@@ -2,6 +2,106 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.3.0] - 2026-02-06
6
+
7
+ ### Added
8
+
9
+ - Added resumption hint printed to stderr on session exit showing command to resume the session (e.g., `Resume this session with claude --resume <session-id>`)
10
+ - New `BlobStore` class for content-addressed storage of large binary data (images) externalized from session files
11
+ - New `getBlobsDir()` function to get path to blob store directory
12
+ - Support for externalizing large images to blob store during session persistence, reducing JSONL file size
13
+ - New blob reference format (`blob:sha256:<hash>`) for tracking externalized image data in sessions
14
+ - Exported `ModeChangeEntry` type for tracking agent mode transitions
15
+ - Support for restoring plan mode state when resuming sessions
16
+ - New `appendModeChange()` method in SessionManager to record mode transitions
17
+ - New `mode` and `modeData` fields in SessionContext to track active agent mode
18
+ - Support for `PI_PACKAGE_DIR` environment variable to override package directory (useful for Nix/Guix store paths)
19
+ - New keybindings for session management: `toggleSessionNamedFilter` (Ctrl+N), `newSession`, `tree`, `fork`, and `resume` actions
20
+ - Support for shell command execution in configuration values (API keys, headers) using `!` prefix, with result caching
21
+ - New `clearOnShrink` display setting to control whether empty rows are cleared when content shrinks
22
+ - New `SlashCommandInfo`, `SlashCommandLocation`, and `SlashCommandSource` types for extension slash command discovery
23
+ - New `getCommands()` method in ExtensionAPI to retrieve available slash commands
24
+ - New `switchSession()` action in ExtensionCommandContext to switch between sessions
25
+ - New `SwitchSessionHandler` type for extension session switching handlers
26
+ - New `getSystemPrompt()` method in ExtensionUIContext to access current system prompt
27
+ - New `getToolsExpanded()` and `setToolsExpanded()` methods in ExtensionUIContext for tool output expansion control
28
+ - New `WriteToolCallEvent` type for write tool call events
29
+ - New `isToolCallEventType()` type guard for tool call events
30
+ - Support for image content in RPC `steer` and `followUp` commands
31
+ - New `GitSource` type and `parseGitUrl()` function for parsing git URLs in plugin system
32
+ - Tool input types exported: `BashToolInput`, `FindToolInput`, `GrepToolInput`, `ReadToolInput`, `WriteToolInput`
33
+ - Support for `@` prefix normalization in file paths (strips leading `@` character)
34
+ - New `parentSessionPath` field in SessionInfo to track forked session origins
35
+ - Skill file relative path resolution against skill directory in system prompt
36
+ - Support for Termux/Android package installation guidance for missing tools
37
+ - Support for puppeteer query handlers (aria/, text/, xpath/, pierce/) in selector parameters across all browser actions
38
+ - Automatic normalization of legacy p- prefixed selectors (p-aria/, p-text/, p-xpath/, p-pierce/) to modern puppeteer query handler syntax
39
+ - Improved click action with intelligent element selection that prioritizes visible, actionable candidates and retries until timeout
40
+ - Enhanced actionability checking for click operations, validating visibility, pointer events, opacity, viewport intersection, and element occlusion
41
+
42
+ ### Changed
43
+
44
+ - Modified `--resume` flag to accept optional session ID or path (e.g., `--resume abc123` or `--resume /path/to/session.jsonl`), with session picker shown when no value provided
45
+ - Consolidated `--session` flag as an alias for `--resume` with value for improved CLI consistency
46
+ - Removed read tool grouping reset logic that was breaking grouping when text or thinking blocks appeared between tool calls
47
+ - Image persistence now externalizes images ≥1KB to content-addressed blob store instead of compressing inline
48
+ - Session loading now automatically resolves blob references back to base64 image data
49
+ - Session forking now resolves blob references in copied entries to ensure data integrity
50
+ - Screenshot tool now automatically compresses images for API content using the same resizing logic as pasted images, reducing payload size while maintaining quality
51
+ - Improved text truncation across tool renderers to respect terminal width constraints and prevent output overflow
52
+ - Enhanced render caching to include width parameter for accurate cache invalidation when terminal width changes
53
+ - HTML export filter now treats `mode_change` entries as settings entries alongside model changes and thinking level changes
54
+ - Replaced ellipsis string (`...`) with Unicode ellipsis character (`…`) throughout UI text and truncation logic for improved typography
55
+ - Improved render performance by introducing caching for tool output blocks and search results to avoid redundant text width and padding computations
56
+ - Enhanced read tool grouping to reset when non-tool content (text/thinking blocks) appears between read calls, preventing unintended coalescing
57
+ - Improved string preview formatting in scalar values to show line counts and truncation indicators for multi-line strings
58
+ - Refactored tool execution component to use shared mutable render state for spinner frames and expansion state, reducing closure overhead
59
+ - Enhanced error handling in tool renderers with logging for renderer failures instead of silent fallbacks
60
+ - Made shell command execution in configuration values asynchronous to prevent blocking the TUI
61
+ - Improved `@` prefix normalization to only strip leading `@` for well-known path syntaxes (absolute paths, home directory, internal URL shorthands) to avoid mangling literal paths
62
+ - Enhanced git URL parsing to strip credentials from repository URLs and validate URL-encoded hash fragments
63
+ - Improved null data handling in task submission to preserve agent output when `submit_result` is called with null/undefined data, enabling fallback text extraction instead of discarding output
64
+ - Updated default model IDs across providers: Claude Sonnet 4.5 → Claude Opus 4.6, Gemini 2.5 Pro → Gemini 3 Pro variants, and others
65
+ - Made model definition fields optional with sensible defaults for local models (Ollama, LM Studio, etc.)
66
+ - Modified custom tool execute signature to reorder parameters: `(toolCallId, params, signal, onUpdate, ctx)` instead of `(toolCallId, params, onUpdate, ctx, signal)`
67
+ - Changed `--version` and `--list-models` flags to exit with `process.exit(0)` instead of returning
68
+ - Improved `--export` flag to exit with `process.exit(0)` on success
69
+ - Enhanced tree selector to preserve last selected ID across filter changes
70
+ - Modified tree navigation to use real leaf ID instead of skipping metadata entries
71
+ - Improved footer path truncation logic to prevent invalid truncation at boundary
72
+ - Enhanced model selector to display selected model name when no matches found
73
+ - Improved RPC client `steer()` and `followUp()` methods to accept optional image content
74
+ - Updated extension loader to check for explicit extension entries in root directory before discovering subdirectories
75
+ - Removed line limiting in custom message component when collapsed
76
+ - Improved API key resolution to support shell command execution via `resolveConfigValue()`
77
+ - Enhanced session branching to preserve parent session path reference
78
+ - Updated selector parameter descriptions to document support for CSS selectors and puppeteer query handlers
79
+ - Modified viewport handling in headless mode to respect custom viewport parameters while disabling viewport in headed mode for better window management
80
+ - Improved click action to use specialized text query handler logic with retry mechanism for better reliability with dynamic content
81
+
82
+ ### Fixed
83
+
84
+ - Fixed background color stability in output blocks when inner content contains SGR reset sequences, preventing background color from being cleared mid-line
85
+ - Fixed spurious ellipsis appended to output lines that were already padded to terminal width by trimming trailing spaces before truncation check
86
+ - Fixed config file parsing to properly handle missing files instead of treating them as errors
87
+ - Fixed truncation indicator in truncate tool to use ellipsis character (…) instead of verbose '[truncated]' suffix
88
+ - Fixed concurrent shell command execution by de-duplicating in-flight requests for the same command
89
+ - Fixed git URL parsing to properly handle URL-encoded characters in hash fragments and reject invalid encodings
90
+ - Fixed task executor to properly handle agents calling `submit_result` with null data by treating it as missing and attempting to extract output from conversation text rather than silently failing
91
+ - Fixed HTML export template to safely handle invalid argument types in tool rendering
92
+ - Fixed path shortening in HTML export to handle non-string paths
93
+ - Fixed custom message rendering to properly display full content without artificial line limits
94
+ - Fixed tree navigation to only restore editor text when editor is empty
95
+ - Fixed session creation to properly track parent session when forking
96
+ - Fixed thinking level initialization to only append change entry for new sessions without existing thinking entries
97
+ - Fixed tool expansion state management to properly propagate through UI context
98
+ - Fixed click action to properly handle text/ query handlers with timeout and retry logic instead of failing immediately
99
+ - Fixed viewport application to only apply when in headless mode or when explicitly requested, preventing conflicts in headed browser mode
100
+
101
+ ### Security
102
+
103
+ - Added support for shell command execution in configuration values with caching to enable secure credential resolution patterns
104
+
5
105
  ## [11.2.1] - 2026-02-05
6
106
 
7
107
  ### Fixed
@@ -162,7 +162,7 @@ function cleanStepText(text: string): string {
162
162
 
163
163
  // Truncate if too long
164
164
  if (cleaned.length > 50) {
165
- cleaned = `${cleaned.slice(0, 47)}...`;
165
+ cleaned = `${cleaned.slice(0, 49)}…`;
166
166
  }
167
167
 
168
168
  return cleaned;
@@ -71,7 +71,7 @@ export default function (pi: HookAPI) {
71
71
 
72
72
  // Run extraction with loader UI
73
73
  const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
74
- const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
74
+ const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}…`);
75
75
  loader.onAbort = () => done(null);
76
76
 
77
77
  // Do the work
@@ -18,7 +18,7 @@ export default function (pi: HookAPI) {
18
18
  turnCount++;
19
19
  const theme = ctx.ui.theme;
20
20
  const spinner = theme.fg("accent", "●");
21
- const text = theme.fg("dim", ` Turn ${turnCount}...`);
21
+ const text = theme.fg("dim", ` Turn ${turnCount}…`);
22
22
  ctx.ui.setStatus("status-demo", spinner + text);
23
23
  });
24
24
 
@@ -28,7 +28,7 @@ console.log("Continued session:", continued.sessionFile);
28
28
  const sessions = SessionManager.list(process.cwd());
29
29
  console.log(`\nFound ${sessions.length} sessions:`);
30
30
  for (const info of sessions.slice(0, 3)) {
31
- console.log(` ${info.id.slice(0, 8)}... - "${info.firstMessage.slice(0, 30)}..."`);
31
+ console.log(` ${info.id.slice(0, 8)} - "${info.firstMessage.slice(0, 30)}"`);
32
32
  }
33
33
 
34
34
  if (sessions.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.2.2",
3
+ "version": "11.3.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -87,15 +87,15 @@
87
87
  "test": "bun test"
88
88
  },
89
89
  "dependencies": {
90
- "@oclif/core": "^4.5.6",
91
- "@oclif/plugin-autocomplete": "^3.2.23",
92
90
  "@mozilla/readability": "0.6.0",
93
- "@oh-my-pi/omp-stats": "11.2.2",
94
- "@oh-my-pi/pi-agent-core": "11.2.2",
95
- "@oh-my-pi/pi-ai": "11.2.2",
96
- "@oh-my-pi/pi-natives": "11.2.2",
97
- "@oh-my-pi/pi-tui": "11.2.2",
98
- "@oh-my-pi/pi-utils": "11.2.2",
91
+ "@oclif/core": "^4.8.0",
92
+ "@oclif/plugin-autocomplete": "^3.2.40",
93
+ "@oh-my-pi/omp-stats": "11.3.0",
94
+ "@oh-my-pi/pi-agent-core": "11.3.0",
95
+ "@oh-my-pi/pi-ai": "11.3.0",
96
+ "@oh-my-pi/pi-natives": "11.3.0",
97
+ "@oh-my-pi/pi-tui": "11.3.0",
98
+ "@oh-my-pi/pi-utils": "11.3.0",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -107,7 +107,7 @@
107
107
  "jsdom": "28.0.0",
108
108
  "marked": "^17.0.1",
109
109
  "node-html-parser": "^7.0.2",
110
- "puppeteer": "^24.36.1",
110
+ "puppeteer": "^24.37.1",
111
111
  "smol-toml": "^1.6.0",
112
112
  "zod": "^4.3.6"
113
113
  },
package/src/cli/args.ts CHANGED
@@ -21,12 +21,11 @@ export interface Args {
21
21
  appendSystemPrompt?: string;
22
22
  thinking?: ThinkingLevel;
23
23
  continue?: boolean;
24
- resume?: boolean;
24
+ resume?: string | true;
25
25
  help?: boolean;
26
26
  version?: boolean;
27
27
  mode?: Mode;
28
28
  noSession?: boolean;
29
- session?: string;
30
29
  sessionDir?: string;
31
30
  models?: string[];
32
31
  tools?: string[];
@@ -76,8 +75,13 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
76
75
  }
77
76
  } else if (arg === "--continue" || arg === "-c") {
78
77
  result.continue = true;
79
- } else if (arg === "--resume" || arg === "-r") {
80
- result.resume = true;
78
+ } else if (arg === "--resume" || arg === "-r" || arg === "--session") {
79
+ const next = args[i + 1];
80
+ if (next && !next.startsWith("-")) {
81
+ result.resume = args[++i];
82
+ } else {
83
+ result.resume = true;
84
+ }
81
85
  } else if (arg === "--provider" && i + 1 < args.length) {
82
86
  result.provider = args[++i];
83
87
  } else if (arg === "--model" && i + 1 < args.length) {
@@ -96,8 +100,6 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
96
100
  result.appendSystemPrompt = args[++i];
97
101
  } else if (arg === "--no-session") {
98
102
  result.noSession = true;
99
- } else if (arg === "--session" && i + 1 < args.length) {
100
- result.session = args[++i];
101
103
  } else if (arg === "--session-dir" && i + 1 < args.length) {
102
104
  result.sessionDir = args[++i];
103
105
  } else if (arg === "--models" && i + 1 < args.length) {
@@ -214,6 +216,7 @@ export function getExtraHelpText(): string {
214
216
 
215
217
  ${chalk.dim("# Configuration")}
216
218
  PI_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
219
+ PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
217
220
  PI_SMOL_MODEL - Override smol/fast model (see --smol)
218
221
  PI_SLOW_MODEL - Override slow/reasoning model (see --slow)
219
222
  PI_PLAN_MODEL - Override planning model (see --plan)
@@ -190,7 +190,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
190
190
  const nativePath = path.join(execDir, nativeAddonName);
191
191
  const nativeTempPath = `${nativePath}.new`;
192
192
 
193
- console.log(chalk.dim(`Downloading ${binaryName}...`));
193
+ console.log(chalk.dim(`Downloading ${binaryName}…`));
194
194
 
195
195
  // Download to temp file
196
196
  const response = await fetch(asset.url, { redirect: "follow" });
@@ -202,7 +202,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
202
202
  await pipeline(response.body, fileStream);
203
203
 
204
204
  // Download native addon
205
- console.log(chalk.dim(`Downloading ${nativeAddonName}...`));
205
+ console.log(chalk.dim(`Downloading ${nativeAddonName}…`));
206
206
 
207
207
  const nativeResponse = await fetch(nativeAsset.url, { redirect: "follow" });
208
208
  if (!nativeResponse.ok || !nativeResponse.body) {
@@ -58,12 +58,9 @@ export default class Index extends Command {
58
58
  char: "c",
59
59
  description: "Continue previous session",
60
60
  }),
61
- resume: Flags.boolean({
61
+ resume: Flags.string({
62
62
  char: "r",
63
- description: "Select a session to resume",
64
- }),
65
- session: Flags.string({
66
- description: "Use specific session file",
63
+ description: "Resume a session (by ID prefix, path, or picker if omitted)",
67
64
  }),
68
65
  "session-dir": Flags.string({
69
66
  description: "Directory for session storage and lookup",
@@ -307,5 +307,5 @@ Call the missing tool(s) now.
307
307
 
308
308
  function truncateToolArg(value: string): string {
309
309
  if (value.length <= 40) return value;
310
- return `${value.slice(0, 37)}...`;
310
+ return `${value.slice(0, 39)}…`;
311
311
  }
@@ -54,7 +54,7 @@ export async function runChangelogFlow({
54
54
 
55
55
  const updated: string[] = [];
56
56
  for (const boundary of boundaries) {
57
- onProgress?.(`Generating entries for ${boundary.changelogPath}...`);
57
+ onProgress?.(`Generating entries for ${boundary.changelogPath}…`);
58
58
  const diff = await git.getDiffForFiles(boundary.files, true);
59
59
  if (!diff.trim()) continue;
60
60
  const stat = await git.getStatForFiles(boundary.files, true);
@@ -108,7 +108,7 @@ export async function applyChangelogProposals({
108
108
  (!proposal.deletions || Object.keys(proposal.deletions).length === 0)
109
109
  )
110
110
  continue;
111
- onProgress?.(`Applying entries for ${proposal.path}...`);
111
+ onProgress?.(`Applying entries for ${proposal.path}…`);
112
112
  const exists = fs.existsSync(proposal.path);
113
113
  if (!exists) {
114
114
  logger.warn("commit changelog path missing", { path: proposal.path });
@@ -26,11 +26,16 @@ export type AppAction =
26
26
  | "togglePlanMode"
27
27
  | "expandTools"
28
28
  | "toggleThinking"
29
+ | "toggleSessionNamedFilter"
29
30
  | "externalEditor"
30
31
  | "historySearch"
31
32
  | "followUp"
32
33
  | "dequeue"
33
- | "pasteImage";
34
+ | "pasteImage"
35
+ | "newSession"
36
+ | "tree"
37
+ | "fork"
38
+ | "resume";
34
39
 
35
40
  /**
36
41
  * All configurable actions.
@@ -60,10 +65,15 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
60
65
  historySearch: "ctrl+r",
61
66
  expandTools: "ctrl+o",
62
67
  toggleThinking: "ctrl+t",
68
+ toggleSessionNamedFilter: "ctrl+n",
63
69
  externalEditor: "ctrl+g",
64
70
  followUp: "ctrl+enter",
65
71
  dequeue: "alt+up",
66
72
  pasteImage: "ctrl+v",
73
+ newSession: [],
74
+ tree: [],
75
+ fork: [],
76
+ resume: [],
67
77
  };
68
78
 
69
79
  /**
@@ -88,10 +98,15 @@ const APP_ACTIONS: AppAction[] = [
88
98
  "historySearch",
89
99
  "expandTools",
90
100
  "toggleThinking",
101
+ "toggleSessionNamedFilter",
91
102
  "externalEditor",
92
103
  "followUp",
93
104
  "dequeue",
94
105
  "pasteImage",
106
+ "newSession",
107
+ "tree",
108
+ "fork",
109
+ "resume",
95
110
  ];
96
111
 
97
112
  function isAppAction(action: string): action is AppAction {
@@ -54,9 +54,10 @@ const OpenAICompatSchema = Type.Object({
54
54
  });
55
55
 
56
56
  // Schema for custom model definition
57
+ // Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
57
58
  const ModelDefinitionSchema = Type.Object({
58
59
  id: Type.String({ minLength: 1 }),
59
- name: Type.String({ minLength: 1 }),
60
+ name: Type.Optional(Type.String({ minLength: 1 })),
60
61
  api: Type.Optional(
61
62
  Type.Union([
62
63
  Type.Literal("openai-completions"),
@@ -68,16 +69,18 @@ const ModelDefinitionSchema = Type.Object({
68
69
  Type.Literal("google-vertex"),
69
70
  ]),
70
71
  ),
71
- reasoning: Type.Boolean(),
72
- input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
73
- cost: Type.Object({
74
- input: Type.Number(),
75
- output: Type.Number(),
76
- cacheRead: Type.Number(),
77
- cacheWrite: Type.Number(),
78
- }),
79
- contextWindow: Type.Number(),
80
- maxTokens: Type.Number(),
72
+ reasoning: Type.Optional(Type.Boolean()),
73
+ input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
74
+ cost: Type.Optional(
75
+ Type.Object({
76
+ input: Type.Number(),
77
+ output: Type.Number(),
78
+ cacheRead: Type.Number(),
79
+ cacheWrite: Type.Number(),
80
+ }),
81
+ ),
82
+ contextWindow: Type.Optional(Type.Number()),
83
+ maxTokens: Type.Optional(Type.Number()),
81
84
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
82
85
  compat: Type.Optional(OpenAICompatSchema),
83
86
  });
@@ -141,10 +144,10 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
141
144
  }
142
145
 
143
146
  if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
144
- if (!modelDef.name) throw new Error(`Provider ${providerName}: model missing "name"`);
145
- if (modelDef.contextWindow <= 0)
147
+ // Validate contextWindow/maxTokens only if provided (they have defaults)
148
+ if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)
146
149
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
147
- if (modelDef.maxTokens <= 0)
150
+ if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)
148
151
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
149
152
  }
150
153
  }
@@ -377,17 +380,19 @@ export class ModelRegistry {
377
380
  }
378
381
 
379
382
  // baseUrl is validated to exist for providers with models
383
+ // Apply defaults for optional fields
384
+ const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
380
385
  models.push({
381
386
  id: modelDef.id,
382
- name: modelDef.name,
387
+ name: modelDef.name ?? modelDef.id,
383
388
  api: api as Api,
384
389
  provider: providerName,
385
390
  baseUrl: providerConfig.baseUrl!,
386
- reasoning: modelDef.reasoning,
387
- input: modelDef.input as ("text" | "image")[],
388
- cost: modelDef.cost,
389
- contextWindow: modelDef.contextWindow,
390
- maxTokens: modelDef.maxTokens,
391
+ reasoning: modelDef.reasoning ?? false,
392
+ input: (modelDef.input ?? ["text"]) as ("text" | "image")[],
393
+ cost: modelDef.cost ?? defaultCost,
394
+ contextWindow: modelDef.contextWindow ?? 128000,
395
+ maxTokens: modelDef.maxTokens ?? 16384,
391
396
  headers,
392
397
  compat: modelDef.compat,
393
398
  } as Model<Api>);
@@ -11,25 +11,25 @@ import type { Settings } from "./settings";
11
11
 
12
12
  /** Default model IDs for each known provider */
13
13
  export const defaultModelPerProvider: Record<KnownProvider, string> = {
14
- "amazon-bedrock": "us.anthropic.claude-sonnet-4-5-20250514-v1:0",
15
- anthropic: "claude-sonnet-4-5",
14
+ "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
15
+ anthropic: "claude-opus-4-6",
16
16
  openai: "gpt-5.1-codex",
17
- "openai-codex": "codex-max",
17
+ "openai-codex": "gpt-5.3-codex",
18
18
  google: "gemini-2.5-pro",
19
19
  "google-gemini-cli": "gemini-2.5-pro",
20
20
  "google-antigravity": "gemini-3-pro-high",
21
- "google-vertex": "gemini-2.5-pro",
21
+ "google-vertex": "gemini-3-pro-preview",
22
22
  "github-copilot": "gpt-4o",
23
- cursor: "claude-sonnet-4-5",
23
+ cursor: "claude-opus-4-6",
24
24
  openrouter: "openai/gpt-5.1-codex",
25
- "vercel-ai-gateway": "claude-sonnet-4-5",
25
+ "vercel-ai-gateway": "anthropic/claude-opus-4-6",
26
26
  xai: "grok-4-fast-non-reasoning",
27
27
  groq: "openai/gpt-oss-120b",
28
28
  cerebras: "zai-glm-4.6",
29
29
  zai: "glm-4.6",
30
30
  mistral: "devstral-medium-latest",
31
- minimax: "MiniMax-M2",
32
- opencode: "claude-opus-4-5",
31
+ minimax: "MiniMax-M2.1",
32
+ opencode: "claude-opus-4-6",
33
33
  "kimi-code": "kimi-k2.5",
34
34
  };
35
35
 
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Resolve configuration values that may be shell commands, environment variables, or literals.
3
+ *
4
+ * Note: command execution is async to avoid blocking the TUI.
5
+ */
6
+
7
+ import { executeShell } from "@oh-my-pi/pi-natives";
8
+
9
+ /** Cache for successful shell command results (persists for process lifetime). */
10
+ const commandResultCache = new Map<string, string>();
11
+
12
+ /** De-duplicates concurrent executions for the same command. */
13
+ const commandInFlight = new Map<string, Promise<string | undefined>>();
14
+
15
+ /**
16
+ * Resolve a config value (API key, header value, etc.) to an actual value.
17
+ * - If starts with "!", executes the rest as a shell command and uses stdout (cached)
18
+ * - Otherwise checks environment variable first, then treats as literal (not cached)
19
+ */
20
+ export async function resolveConfigValue(config: string): Promise<string | undefined> {
21
+ if (config.startsWith("!")) {
22
+ return await executeCommand(config);
23
+ }
24
+ const envValue = process.env[config];
25
+ return envValue || config;
26
+ }
27
+
28
+ async function executeCommand(commandConfig: string): Promise<string | undefined> {
29
+ const cached = commandResultCache.get(commandConfig);
30
+ if (cached !== undefined) {
31
+ return cached;
32
+ }
33
+
34
+ const existing = commandInFlight.get(commandConfig);
35
+ if (existing) {
36
+ return await existing;
37
+ }
38
+
39
+ const command = commandConfig.slice(1);
40
+ const promise = runShellCommand(command, 10_000)
41
+ .then(result => {
42
+ if (result !== undefined) {
43
+ commandResultCache.set(commandConfig, result);
44
+ }
45
+ return result;
46
+ })
47
+ .finally(() => {
48
+ commandInFlight.delete(commandConfig);
49
+ });
50
+
51
+ commandInFlight.set(commandConfig, promise);
52
+ return await promise;
53
+ }
54
+
55
+ async function runShellCommand(command: string, timeoutMs: number): Promise<string | undefined> {
56
+ try {
57
+ let output = "";
58
+ const result = await executeShell({ command, timeoutMs }, chunk => {
59
+ output += chunk;
60
+ });
61
+ if (result.timedOut || result.exitCode !== 0) {
62
+ return undefined;
63
+ }
64
+ const trimmed = output.trim();
65
+ return trimmed.length > 0 ? trimmed : undefined;
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resolve all header values using the same resolution logic as API keys.
73
+ */
74
+ export async function resolveHeaders(
75
+ headers: Record<string, string> | undefined,
76
+ ): Promise<Record<string, string> | undefined> {
77
+ if (!headers) return undefined;
78
+ const resolved: Record<string, string> = {};
79
+ for (const [key, value] of Object.entries(headers)) {
80
+ const resolvedValue = await resolveConfigValue(value);
81
+ if (resolvedValue) {
82
+ resolved[key] = resolvedValue;
83
+ }
84
+ }
85
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
86
+ }
87
+
88
+ /** Clear the config value command cache. Exported for testing. */
89
+ export function clearConfigValueCache(): void {
90
+ commandResultCache.clear();
91
+ commandInFlight.clear();
92
+ }
@@ -242,6 +242,15 @@ export const SETTINGS_SCHEMA = {
242
242
  default: true, // will be computed based on platform if undefined
243
243
  ui: { tab: "display", label: "Hardware cursor", description: "Show terminal cursor for IME support" },
244
244
  },
245
+ clearOnShrink: {
246
+ type: "boolean",
247
+ default: false,
248
+ ui: {
249
+ tab: "display",
250
+ label: "Clear on shrink",
251
+ description: "Clear empty rows when content shrinks (may cause flicker)",
252
+ },
253
+ },
245
254
  extensions: { type: "array", default: [] as string[] },
246
255
  enabledModels: { type: "array", default: [] as string[] },
247
256
  disabledProviders: { type: "array", default: [] as string[] },
package/src/config.ts CHANGED
@@ -35,6 +35,14 @@ const priorityList = [
35
35
  * Walk up from import.meta.dir until we find package.json, or fall back to cwd.
36
36
  */
37
37
  export function getPackageDir(): string {
38
+ // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
39
+ const envDir = process.env.PI_PACKAGE_DIR;
40
+ if (envDir) {
41
+ if (envDir === "~") return os.homedir();
42
+ if (envDir.startsWith("~/")) return os.homedir() + envDir.slice(1);
43
+ return envDir;
44
+ }
45
+
38
46
  let dir = import.meta.dir;
39
47
  while (dir !== path.dirname(dir)) {
40
48
  if (fs.existsSync(path.join(dir, "package.json"))) {
@@ -218,7 +226,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
218
226
  }
219
227
  return this.#storeCache({ value: parsed, status: "ok" });
220
228
  } catch (error) {
221
- if (!isEnoent(error)) {
229
+ if (isEnoent(error)) {
222
230
  return this.#storeCache({ status: "not-found" });
223
231
  }
224
232
  logger.warn("Failed to parse config file", { path: this.path(), error });
@@ -280,6 +288,11 @@ export function getPromptsDir(): string {
280
288
  return path.join(getAgentDir(), "prompts");
281
289
  }
282
290
 
291
+ /** Get path to content-addressed blob store directory */
292
+ export function getBlobsDir(): string {
293
+ return path.join(getAgentDir(), "blobs");
294
+ }
295
+
283
296
  /** Get path to sessions directory */
284
297
  export function getSessionsDir(): string {
285
298
  return path.join(getAgentDir(), "sessions");
@@ -473,6 +473,10 @@
473
473
  display: block;
474
474
  }
475
475
 
476
+ .ansi-line {
477
+ white-space: pre-wrap;
478
+ }
479
+
476
480
  .tool-images {
477
481
  }
478
482
 
@@ -666,6 +670,9 @@
666
670
  color: var(--error);
667
671
  padding: 0 var(--line-height);
668
672
  }
673
+ .tool-error {
674
+ color: var(--error);
675
+ }
669
676
 
670
677
  /* Images */
671
678
  .message-images {