@oh-my-pi/pi-coding-agent 3.13.1337 → 3.15.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 (149) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/docs/theme.md +38 -5
  3. package/examples/sdk/11-sessions.ts +2 -2
  4. package/package.json +7 -4
  5. package/src/cli/file-processor.ts +51 -2
  6. package/src/cli/plugin-cli.ts +25 -19
  7. package/src/cli/update-cli.ts +4 -3
  8. package/src/core/agent-session.ts +31 -4
  9. package/src/core/compaction/branch-summarization.ts +4 -32
  10. package/src/core/compaction/compaction.ts +6 -84
  11. package/src/core/compaction/utils.ts +2 -3
  12. package/src/core/custom-tools/types.ts +2 -0
  13. package/src/core/export-html/index.ts +1 -1
  14. package/src/core/hooks/index.ts +1 -1
  15. package/src/core/hooks/tool-wrapper.ts +0 -1
  16. package/src/core/hooks/types.ts +2 -2
  17. package/src/core/plugins/doctor.ts +9 -1
  18. package/src/core/sdk.ts +2 -1
  19. package/src/core/session-manager.ts +552 -41
  20. package/src/core/settings-manager.ts +174 -0
  21. package/src/core/system-prompt.ts +9 -14
  22. package/src/core/title-generator.ts +2 -8
  23. package/src/core/tools/ask.ts +19 -37
  24. package/src/core/tools/bash.ts +2 -37
  25. package/src/core/tools/edit.ts +2 -9
  26. package/src/core/tools/exa/render.ts +52 -48
  27. package/src/core/tools/find.ts +10 -8
  28. package/src/core/tools/grep.ts +45 -17
  29. package/src/core/tools/ls.ts +22 -2
  30. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  31. package/src/core/tools/lsp/clients/index.ts +49 -0
  32. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  33. package/src/core/tools/lsp/config.ts +3 -0
  34. package/src/core/tools/lsp/index.ts +107 -55
  35. package/src/core/tools/lsp/render.ts +192 -79
  36. package/src/core/tools/lsp/types.ts +27 -0
  37. package/src/core/tools/lsp/utils.ts +62 -22
  38. package/src/core/tools/notebook.ts +9 -1
  39. package/src/core/tools/output.ts +37 -14
  40. package/src/core/tools/read.ts +349 -34
  41. package/src/core/tools/renderers.ts +290 -89
  42. package/src/core/tools/review.ts +12 -5
  43. package/src/core/tools/task/agents.ts +5 -5
  44. package/src/core/tools/task/commands.ts +3 -3
  45. package/src/core/tools/task/executor.ts +33 -1
  46. package/src/core/tools/task/index.ts +93 -6
  47. package/src/core/tools/task/render.ts +147 -66
  48. package/src/core/tools/task/types.ts +14 -9
  49. package/src/core/tools/web-fetch.ts +242 -103
  50. package/src/core/tools/web-search/index.ts +64 -20
  51. package/src/core/tools/web-search/providers/exa.ts +68 -172
  52. package/src/core/tools/web-search/render.ts +264 -74
  53. package/src/core/tools/write.ts +2 -8
  54. package/src/main.ts +10 -6
  55. package/src/modes/cleanup.ts +23 -0
  56. package/src/modes/index.ts +9 -4
  57. package/src/modes/interactive/components/bash-execution.ts +6 -3
  58. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  60. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  61. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  62. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  63. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  64. package/src/modes/interactive/components/hook-message.ts +2 -2
  65. package/src/modes/interactive/components/hook-selector.ts +1 -1
  66. package/src/modes/interactive/components/model-selector.ts +22 -9
  67. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  68. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  69. package/src/modes/interactive/components/session-selector.ts +9 -6
  70. package/src/modes/interactive/components/settings-defs.ts +285 -1
  71. package/src/modes/interactive/components/settings-selector.ts +176 -3
  72. package/src/modes/interactive/components/status-line/index.ts +4 -0
  73. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  74. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  75. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  76. package/src/modes/interactive/components/status-line/types.ts +81 -0
  77. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  78. package/src/modes/interactive/components/status-line.ts +169 -233
  79. package/src/modes/interactive/components/tool-execution.ts +446 -211
  80. package/src/modes/interactive/components/tree-selector.ts +17 -6
  81. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  82. package/src/modes/interactive/components/welcome.ts +27 -19
  83. package/src/modes/interactive/interactive-mode.ts +98 -13
  84. package/src/modes/interactive/theme/dark.json +3 -2
  85. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  86. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  87. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  88. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  89. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  90. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  91. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  92. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  93. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  94. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  95. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  96. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  97. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  98. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  99. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  100. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  101. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  102. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  103. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  104. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  105. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  106. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  107. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  108. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  109. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  110. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  111. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  112. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  113. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  114. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  115. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  116. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  117. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  118. package/src/modes/interactive/theme/light.json +3 -2
  119. package/src/modes/interactive/theme/theme-schema.json +120 -4
  120. package/src/modes/interactive/theme/theme.ts +1228 -14
  121. package/src/prompts/branch-summary-preamble.md +3 -0
  122. package/src/prompts/branch-summary.md +28 -0
  123. package/src/prompts/compaction-summary.md +34 -0
  124. package/src/prompts/compaction-turn-prefix.md +16 -0
  125. package/src/prompts/compaction-update-summary.md +41 -0
  126. package/src/prompts/init.md +30 -0
  127. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  128. package/src/prompts/summarization-system.md +3 -0
  129. package/src/prompts/system-prompt.md +27 -0
  130. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  131. package/src/prompts/title-system.md +8 -0
  132. package/src/prompts/tools/ask.md +24 -0
  133. package/src/prompts/tools/bash.md +23 -0
  134. package/src/prompts/tools/edit.md +9 -0
  135. package/src/prompts/tools/find.md +6 -0
  136. package/src/prompts/tools/grep.md +12 -0
  137. package/src/prompts/tools/lsp.md +14 -0
  138. package/src/prompts/tools/output.md +23 -0
  139. package/src/prompts/tools/read.md +25 -0
  140. package/src/prompts/tools/web-fetch.md +8 -0
  141. package/src/prompts/tools/web-search.md +10 -0
  142. package/src/prompts/tools/write.md +10 -0
  143. package/src/commands/init.md +0 -20
  144. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  145. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  146. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  147. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  148. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  149. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,94 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.15.0] - 2026-01-05
6
+
7
+ ### Added
8
+
9
+ - Added spinner type variants (status and activity) with distinct animation frames per symbol preset
10
+ - Added animated spinner for task tool progress display during subagent execution
11
+ - Added language/file type icons for read tool output with support for 35+ file types
12
+ - Added async cleanup registry for graceful session flush on SIGINT, SIGTERM, and SIGHUP signals
13
+ - Added subagent token usage aggregation to session statistics and task tool results
14
+ - Added streaming NDJSON writer for session persistence with proper backpressure handling
15
+ - Added `flush()` method to SessionManager for explicit control over pending write completion
16
+ - Added `/exit` slash command to exit the application from interactive mode
17
+ - Added fuzzy path matching suggestions when read tool encounters file-not-found errors, showing closest matches using Levenshtein distance
18
+ - Added `status.shadowed` symbol for theme customization to properly indicate shadowed extension state
19
+ - Added Biome CLI-based linter client as alternative to LSP for more reliable diagnostics
20
+ - Added LinterClient interface for pluggable formatter/linter implementations
21
+ - Added status line segment editor for arranging and toggling status line components
22
+ - Added status line presets (default, minimal, compact, developer, balanced) for quick configuration
23
+ - Added status line separator styles (powerline, powerline-thin, arrow, slash, pipe, space)
24
+ - Added configurable status line segments including time, hostname, and subagent count
25
+ - Added symbol customization via theme overrides for icons, separators, and glyphs
26
+ - Added 30+ built-in color themes including Catppuccin, Dracula, Nord, Gruvbox, Tokyo Night, and more
27
+ - Added configurable status line with customizable segments, presets, and separators
28
+ - Added status line segment editor for arranging and toggling status line components
29
+ - Added symbol preset setting to switch between Unicode, Nerd Font, and ASCII glyphs
30
+ - Added file size limit (20MB) for image files to prevent memory issues during serialization
31
+
32
+ ### Changed
33
+
34
+ - Changed `isError` property in tool result events to be optional instead of required
35
+ - Changed `SessionManager.open()` and `SessionManager.continueRecent()` to async methods for proper initialization
36
+ - Changed session file writes to use atomic rename pattern with fsync for crash-safe persistence
37
+ - Changed read tool display to show file type icons and metadata inline with path
38
+ - Changed `AgentSession.dispose()` to async method that flushes pending writes before cleanup
39
+ - Changed read tool result display to hide content by default with expand hint, showing only metadata until expanded
40
+ - Changed diagnostics display to group messages by file with tree structure and severity icons
41
+ - Changed diff stats formatting to use colored +/- indicators with slash separators
42
+ - Changed session persistence to use streaming writes instead of synchronous file appends for better performance
43
+ - Changed read tool to automatically redirect to ls when given a directory path instead of a file
44
+ - Changed tool description prompts to be more concise with clearer usage guidelines and structured formatting
45
+ - Moved tool description prompts from inline strings to external markdown files in `src/prompts/tools/` directory for better maintainability
46
+ - Changed Exa web search provider from MCP protocol to direct REST API for simpler integration
47
+ - Changed web search result rendering to handle malformed response data with fallback text display
48
+ - Changed compaction prompts to preserve tool outputs, command results, and repository state in context summaries
49
+ - Changed init prompt to include runtime/tooling preferences section and improved formatting guidelines
50
+ - Changed reviewer prompt to require evidence-backed findings anchored to diff hunks with stricter suggestion block formatting
51
+ - Changed system prompt to include explicit core behavior guidelines for task completion and progress updates
52
+ - Changed task prompt to emphasize end-to-end task completion and tool verification
53
+ - Moved all prompt templates from inline strings to external markdown files in `src/prompts/` directory for better maintainability
54
+ - Changed tool result renderers to use structured tree layouts with consistent expand hints and truncation indicators
55
+ - Changed grep, find, and ls tools to show scope path and detailed truncation reasons in output
56
+ - Changed web search and web fetch result rendering to display structured metadata sections with bounded content previews
57
+ - Changed task/subagent progress rendering to use badge-style status labels and structured output sections
58
+ - Changed notebook tool to display cell content preview with line counts
59
+ - Changed ask tool result to show checkbox-style selection indicators
60
+ - Changed output tool to include provenance metadata and content previews for retrieved outputs
61
+ - Changed collapsed tool views to show consistent "Ctrl+O to expand" hints with remaining item counts
62
+ - Changed Biome integration to use CLI instead of LSP to avoid stale diagnostics issues
63
+ - Changed hardcoded UI symbols throughout codebase to use theme-configurable glyphs
64
+ - Changed tree drawing characters to use theme-defined box-drawing symbols
65
+ - Changed status line rendering to support left/right segment positioning with separators
66
+ - Changed hardcoded UI symbols to use theme-configurable glyphs throughout the interface
67
+ - Changed tree drawing characters to use theme-defined box-drawing symbols
68
+ - Changed CLI image attachments to resize if larger than 2048px (fit within 1920x1080) and convert >2MB images to JPEG
69
+
70
+ ### Removed
71
+
72
+ - Removed custom renderers for ls, find, and grep tools in favor of generic tool display
73
+
74
+ ### Fixed
75
+
76
+ - Fixed spinner animation crash when spinner frames array is empty by adding length check
77
+ - Fixed session persistence to properly await all queued writes before closing or switching sessions
78
+ - Fixed session persistence to truncate oversized content blocks before writing to prevent memory exhaustion
79
+ - Fixed extension list and inspector panel to use correct symbols for disabled and shadowed states instead of reusing unrelated status icons
80
+ - Fixed token counting for subagent progress to handle different usage object formats (camelCase and snake_case)
81
+ - Fixed image file handling by adding 20MB size limit to prevent memory issues during serialization
82
+ - Fixed session persistence to truncate oversized entries before writing JSONL to prevent out-of-memory errors
83
+
84
+ ## [3.14.0] - 2026-01-04
85
+ ### Added
86
+
87
+ - Added `getUsageStatistics()` method to SessionManager for tracking cumulative token usage and costs across session messages
88
+
89
+ ### Changed
90
+
91
+ - Changed status line to display usage statistics more efficiently by using centralized session statistics instead of recalculating from entries
92
+
5
93
  ## [3.13.1337] - 2026-01-04
6
94
 
7
95
  ## [3.9.1337] - 2026-01-04
package/docs/theme.md CHANGED
@@ -132,7 +132,7 @@ Themes are defined in JSON files with the following structure:
132
132
 
133
133
  ```json
134
134
  {
135
- "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
135
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
136
136
  "name": "my-theme",
137
137
  "vars": {
138
138
  "blue": "#0066cc",
@@ -149,6 +149,39 @@ Themes are defined in JSON files with the following structure:
149
149
  }
150
150
  ```
151
151
 
152
+ ## Symbols
153
+
154
+ Themes can also customize specific UI symbols (icons, separators, bullets, etc.). Use `symbols.preset` to set a theme default (overridden by user settings), and `symbols.overrides` to override individual keys.
155
+
156
+ Example:
157
+
158
+ ```json
159
+ {
160
+ "symbols": {
161
+ "preset": "ascii",
162
+ "overrides": {
163
+ "icon.model": "[M]",
164
+ "sep.powerlineLeft": ">",
165
+ "sep.powerlineRight": "<"
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ Symbol keys by category:
172
+
173
+ - Status: `status.success`, `status.error`, `status.warning`, `status.info`, `status.pending`, `status.disabled`, `status.enabled`, `status.running`, `status.shadowed`, `status.aborted`
174
+ - Navigation: `nav.cursor`, `nav.selected`, `nav.expand`, `nav.collapse`, `nav.back`
175
+ - Tree: `tree.branch`, `tree.last`, `tree.vertical`, `tree.horizontal`, `tree.hook`
176
+ - Boxes (rounded): `boxRound.topLeft`, `boxRound.topRight`, `boxRound.bottomLeft`, `boxRound.bottomRight`, `boxRound.horizontal`, `boxRound.vertical`
177
+ - Boxes (sharp): `boxSharp.topLeft`, `boxSharp.topRight`, `boxSharp.bottomLeft`, `boxSharp.bottomRight`, `boxSharp.horizontal`, `boxSharp.vertical`, `boxSharp.cross`, `boxSharp.teeDown`, `boxSharp.teeUp`, `boxSharp.teeRight`, `boxSharp.teeLeft`
178
+ - Separators: `sep.powerline`, `sep.powerlineThin`, `sep.powerlineLeft`, `sep.powerlineRight`, `sep.powerlineThinLeft`, `sep.powerlineThinRight`, `sep.dot`, `sep.slash`, `sep.pipe`
179
+ - Icons: `icon.model`, `icon.folder`, `icon.file`, `icon.git`, `icon.branch`, `icon.tokens`, `icon.context`, `icon.cost`, `icon.time`, `icon.pi`, `icon.agents`, `icon.cache`, `icon.input`, `icon.output`, `icon.host`, `icon.session`, `icon.package`, `icon.warning`, `icon.rewind`, `icon.auto`, `icon.extensionSkill`, `icon.extensionTool`, `icon.extensionSlashCommand`, `icon.extensionMcp`, `icon.extensionRule`, `icon.extensionHook`, `icon.extensionPrompt`, `icon.extensionContextFile`, `icon.extensionInstruction`
180
+ - Thinking: `thinking.minimal`, `thinking.low`, `thinking.medium`, `thinking.high`, `thinking.xhigh`
181
+ - Checkboxes: `checkbox.checked`, `checkbox.unchecked`
182
+ - Formatting: `format.ellipsis`, `format.bullet`, `format.dash`
183
+ - Markdown: `md.quoteBorder`, `md.hrChar`, `md.bullet`
184
+
152
185
  ### Color Values
153
186
 
154
187
  Four formats are supported:
@@ -253,7 +286,7 @@ Custom themes are loaded from `~/.omp/agent/themes/*.json`.
253
286
 
254
287
  ```json
255
288
  {
256
- "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
289
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
257
290
  "name": "my-theme",
258
291
  "vars": {
259
292
  "primary": "#00aaff",
@@ -448,14 +481,14 @@ Error loading theme 'my-theme':
448
481
  For editor support, the JSON schema is available at:
449
482
 
450
483
  ```
451
- https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json
484
+ https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json
452
485
  ```
453
486
 
454
487
  Add to your theme file for auto-completion and validation:
455
488
 
456
489
  ```json
457
490
  {
458
- "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
491
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
459
492
  ...
460
493
  }
461
494
  ```
@@ -552,7 +585,7 @@ function getMarkdownTheme(): MarkdownTheme {
552
585
  }
553
586
 
554
587
  // Create markdown with theme
555
- const md = new Markdown(text, 1, 1, { bgColor: theme.bg("userMessageBg") }, getMarkdownTheme());
588
+ const md = new Markdown(text, 1, 1, getMarkdownTheme(), { bgColor: theme.bg("userMessageBg") });
556
589
  ```
557
590
 
558
591
  This approach:
@@ -20,7 +20,7 @@ console.log("New session file:", newSession.sessionFile);
20
20
 
21
21
  // Continue most recent session (or create new if none)
22
22
  const { session: continued, modelFallbackMessage } = await createAgentSession({
23
- sessionManager: SessionManager.continueRecent(process.cwd()),
23
+ sessionManager: await SessionManager.continueRecent(process.cwd()),
24
24
  });
25
25
  if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
26
26
  console.log("Continued session:", continued.sessionFile);
@@ -34,7 +34,7 @@ for (const info of sessions.slice(0, 3)) {
34
34
 
35
35
  if (sessions.length > 0) {
36
36
  const { session: opened } = await createAgentSession({
37
- sessionManager: SessionManager.open(sessions[0].path),
37
+ sessionManager: await SessionManager.open(sessions[0].path),
38
38
  });
39
39
  console.log(`\nOpened: ${opened.sessionId}`);
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.13.1337",
3
+ "version": "3.15.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-agent-core": "3.13.1337",
43
- "@oh-my-pi/pi-ai": "3.13.1337",
44
- "@oh-my-pi/pi-tui": "3.13.1337",
42
+ "@oh-my-pi/pi-agent-core": "3.15.0",
43
+ "@oh-my-pi/pi-ai": "3.15.0",
44
+ "@oh-my-pi/pi-tui": "3.15.0",
45
45
  "@sinclair/typebox": "^0.34.46",
46
46
  "ajv": "^8.17.1",
47
47
  "chalk": "^5.5.0",
@@ -53,15 +53,18 @@
53
53
  "marked": "^15.0.12",
54
54
  "minimatch": "^10.1.1",
55
55
  "nanoid": "^5.1.6",
56
+ "ndjson": "^2.0.0",
56
57
  "node-html-parser": "^6.1.13",
57
58
  "smol-toml": "^1.6.0",
58
59
  "strip-ansi": "^7.1.2",
60
+ "sharp": "^0.34.2",
59
61
  "winston": "^3.17.0",
60
62
  "winston-daily-rotate-file": "^5.0.0",
61
63
  "yaml": "^2.8.2"
62
64
  },
63
65
  "devDependencies": {
64
66
  "@types/diff": "^7.0.2",
67
+ "@types/ndjson": "^2.0.4",
65
68
  "@types/node": "^24.3.0",
66
69
  "vitest": "^3.2.4"
67
70
  },
@@ -6,6 +6,7 @@ import { access, readFile, stat } from "node:fs/promises";
6
6
  import { resolve } from "node:path";
7
7
  import type { ImageContent } from "@oh-my-pi/pi-ai";
8
8
  import chalk from "chalk";
9
+ import sharp from "sharp";
9
10
  import { resolveReadPath } from "../core/tools/path-utils";
10
11
  import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
11
12
 
@@ -14,6 +15,53 @@ export interface ProcessedFiles {
14
15
  images: ImageContent[];
15
16
  }
16
17
 
18
+ const RESIZE_TRIGGER_MAX_DIMENSION = 2048;
19
+ const MAX_RESIZE_WIDTH = 1920;
20
+ const MAX_RESIZE_HEIGHT = 1080;
21
+ const JPEG_CONVERT_THRESHOLD_BYTES = 2 * 1024 * 1024;
22
+ const JPEG_QUALITY = 85;
23
+
24
+ async function processImageAttachment(buffer: Buffer, mimeType: string): Promise<{ buffer: Buffer; mimeType: string }> {
25
+ const metadata = await sharp(buffer, { failOnError: false }).metadata();
26
+ const width = metadata.width ?? 0;
27
+ const height = metadata.height ?? 0;
28
+ const maxDim = Math.max(width, height);
29
+ const shouldResize = width > 0 && height > 0 && maxDim > RESIZE_TRIGGER_MAX_DIMENSION;
30
+ const shouldConvertToJpeg = buffer.length > JPEG_CONVERT_THRESHOLD_BYTES;
31
+
32
+ if (!shouldResize && !shouldConvertToJpeg) {
33
+ return { buffer, mimeType };
34
+ }
35
+
36
+ let pipeline = sharp(buffer, { failOnError: false });
37
+ if (shouldResize) {
38
+ pipeline = pipeline.resize({
39
+ width: MAX_RESIZE_WIDTH,
40
+ height: MAX_RESIZE_HEIGHT,
41
+ fit: "inside",
42
+ withoutEnlargement: true,
43
+ });
44
+ }
45
+
46
+ if (shouldConvertToJpeg) {
47
+ pipeline = pipeline.jpeg({ quality: JPEG_QUALITY });
48
+ return { buffer: await pipeline.toBuffer(), mimeType: "image/jpeg" };
49
+ }
50
+
51
+ if (mimeType === "image/png") {
52
+ pipeline = pipeline.png();
53
+ } else if (mimeType === "image/webp") {
54
+ pipeline = pipeline.webp();
55
+ } else if (mimeType === "image/gif") {
56
+ pipeline = pipeline.gif();
57
+ } else {
58
+ pipeline = pipeline.jpeg({ quality: JPEG_QUALITY });
59
+ return { buffer: await pipeline.toBuffer(), mimeType: "image/jpeg" };
60
+ }
61
+
62
+ return { buffer: await pipeline.toBuffer(), mimeType };
63
+ }
64
+
17
65
  /** Process @file arguments into text content and image attachments */
18
66
  export async function processFileArguments(fileArgs: string[]): Promise<ProcessedFiles> {
19
67
  let text = "";
@@ -43,11 +91,12 @@ export async function processFileArguments(fileArgs: string[]): Promise<Processe
43
91
  if (mimeType) {
44
92
  // Handle image file
45
93
  const content = await readFile(absolutePath);
46
- const base64Content = content.toString("base64");
94
+ const processed = await processImageAttachment(content, mimeType);
95
+ const base64Content = processed.buffer.toString("base64");
47
96
 
48
97
  const attachment: ImageContent = {
49
98
  type: "image",
50
- mimeType,
99
+ mimeType: processed.mimeType,
51
100
  data: base64Content,
52
101
  };
53
102
 
@@ -7,6 +7,7 @@
7
7
  import chalk from "chalk";
8
8
  import { APP_NAME } from "../config";
9
9
  import { PluginManager, parseSettingValue, validateSetting } from "../core/plugins/index";
10
+ import { theme } from "../modes/interactive/theme/theme";
10
11
 
11
12
  // =============================================================================
12
13
  // Types
@@ -173,7 +174,7 @@ async function handleInstall(
173
174
  if (flags.dryRun) {
174
175
  console.log(chalk.dim(`[dry-run] Would install ${spec}`));
175
176
  } else {
176
- console.log(chalk.green(`✓ Installed ${result.name}@${result.version}`));
177
+ console.log(chalk.green(`${theme.status.success} Installed ${result.name}@${result.version}`));
177
178
  if (result.enabledFeatures && result.enabledFeatures.length > 0) {
178
179
  console.log(chalk.dim(` Features: ${result.enabledFeatures.join(", ")}`));
179
180
  }
@@ -183,7 +184,7 @@ async function handleInstall(
183
184
  }
184
185
  }
185
186
  } catch (err) {
186
- console.error(chalk.red(`✗ Failed to install ${spec}: ${err}`));
187
+ console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
187
188
  process.exit(1);
188
189
  }
189
190
  }
@@ -202,10 +203,10 @@ async function handleUninstall(manager: PluginManager, packages: string[], flags
202
203
  if (flags.json) {
203
204
  console.log(JSON.stringify({ uninstalled: name }));
204
205
  } else {
205
- console.log(chalk.green(`✓ Uninstalled ${name}`));
206
+ console.log(chalk.green(`${theme.status.success} Uninstalled ${name}`));
206
207
  }
207
208
  } catch (err) {
208
- console.error(chalk.red(`✗ Failed to uninstall ${name}: ${err}`));
209
+ console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`));
209
210
  process.exit(1);
210
211
  }
211
212
  }
@@ -226,8 +227,9 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
226
227
  }
227
228
 
228
229
  console.log(chalk.bold("Installed Plugins:\n"));
230
+
229
231
  for (const plugin of plugins) {
230
- const status = plugin.enabled ? chalk.green("●") : chalk.dim("○");
232
+ const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
231
233
  const nameVersion = `${plugin.name}@${plugin.version}`;
232
234
  console.log(`${status} ${nameVersion}`);
233
235
 
@@ -265,10 +267,10 @@ async function handleLink(manager: PluginManager, paths: string[], flags: { json
265
267
  if (flags.json) {
266
268
  console.log(JSON.stringify(result, null, 2));
267
269
  } else {
268
- console.log(chalk.green(`✓ Linked ${result.name} from ${paths[0]}`));
270
+ console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${paths[0]}`));
269
271
  }
270
272
  } catch (err) {
271
- console.error(chalk.red(`✗ Failed to link: ${err}`));
273
+ console.error(chalk.red(`${theme.status.error} Failed to link: ${err}`));
272
274
  process.exit(1);
273
275
  }
274
276
  }
@@ -285,10 +287,14 @@ async function handleDoctor(manager: PluginManager, flags: { json?: boolean; fix
285
287
 
286
288
  for (const check of checks) {
287
289
  const icon =
288
- check.status === "ok" ? chalk.green("✓") : check.status === "warning" ? chalk.yellow("!") : chalk.red("✗");
290
+ check.status === "ok"
291
+ ? chalk.green(theme.status.success)
292
+ : check.status === "warning"
293
+ ? chalk.yellow(theme.status.warning)
294
+ : chalk.red(theme.status.error);
289
295
  console.log(`${icon} ${check.name}: ${check.message}`);
290
296
  if (check.fixed) {
291
- console.log(chalk.dim(` Fixed`));
297
+ console.log(chalk.dim(` ${theme.nav.cursor} Fixed`));
292
298
  }
293
299
  }
294
300
 
@@ -361,7 +367,7 @@ async function handleFeatures(
361
367
  }
362
368
 
363
369
  await manager.setEnabledFeatures(pluginName, [...currentFeatures]);
364
- console.log(chalk.green(`✓ Updated features for ${pluginName}`));
370
+ console.log(chalk.green(`${theme.status.success} Updated features for ${pluginName}`));
365
371
  }
366
372
 
367
373
  // Display current state
@@ -392,7 +398,7 @@ async function handleFeatures(
392
398
  const enabledSet = new Set(updatedFeatures ?? []);
393
399
  for (const [name, feat] of Object.entries(plugin.manifest.features)) {
394
400
  const enabled = enabledSet.has(name);
395
- const icon = enabled ? chalk.green("●") : chalk.dim("○");
401
+ const icon = enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
396
402
  const defaultLabel = feat.default ? chalk.dim(" (default)") : "";
397
403
  console.log(`${icon} ${name}${defaultLabel}`);
398
404
  if (feat.description) {
@@ -507,7 +513,7 @@ async function handleConfig(
507
513
  }
508
514
 
509
515
  manager.setPluginSetting(pluginName, key, value);
510
- console.log(chalk.green(`✓ Set ${key}`));
516
+ console.log(chalk.green(`${theme.status.success} Set ${key}`));
511
517
  break;
512
518
  }
513
519
 
@@ -518,7 +524,7 @@ async function handleConfig(
518
524
  }
519
525
 
520
526
  manager.deletePluginSetting(pluginName, key);
521
- console.log(chalk.green(`✓ Deleted ${key}`));
527
+ console.log(chalk.green(`${theme.status.success} Deleted ${key}`));
522
528
  break;
523
529
  }
524
530
 
@@ -554,10 +560,10 @@ async function handleConfigValidate(manager: PluginManager, flags: { json?: bool
554
560
  }
555
561
 
556
562
  if (results.length === 0) {
557
- console.log(chalk.green("✓ All settings valid"));
563
+ console.log(chalk.green(`${theme.status.success} All settings valid`));
558
564
  } else {
559
565
  for (const { plugin, key, error } of results) {
560
- console.log(chalk.red(`✗ ${plugin}.${key}: ${error}`));
566
+ console.log(chalk.red(`${theme.status.error} ${plugin}.${key}: ${error}`));
561
567
  }
562
568
  process.exit(1);
563
569
  }
@@ -576,10 +582,10 @@ async function handleEnable(manager: PluginManager, plugins: string[], flags: {
576
582
  if (flags.json) {
577
583
  console.log(JSON.stringify({ enabled: name }));
578
584
  } else {
579
- console.log(chalk.green(`✓ Enabled ${name}`));
585
+ console.log(chalk.green(`${theme.status.success} Enabled ${name}`));
580
586
  }
581
587
  } catch (err) {
582
- console.error(chalk.red(`✗ Failed to enable ${name}: ${err}`));
588
+ console.error(chalk.red(`${theme.status.error} Failed to enable ${name}: ${err}`));
583
589
  process.exit(1);
584
590
  }
585
591
  }
@@ -598,10 +604,10 @@ async function handleDisable(manager: PluginManager, plugins: string[], flags: {
598
604
  if (flags.json) {
599
605
  console.log(JSON.stringify({ disabled: name }));
600
606
  } else {
601
- console.log(chalk.green(`✓ Disabled ${name}`));
607
+ console.log(chalk.green(`${theme.status.success} Disabled ${name}`));
602
608
  }
603
609
  } catch (err) {
604
- console.error(chalk.red(`✗ Failed to disable ${name}: ${err}`));
610
+ console.error(chalk.red(`${theme.status.error} Failed to disable ${name}: ${err}`));
605
611
  process.exit(1);
606
612
  }
607
613
  }
@@ -12,6 +12,7 @@ import { Readable } from "node:stream";
12
12
  import { pipeline } from "node:stream/promises";
13
13
  import chalk from "chalk";
14
14
  import { APP_NAME, VERSION } from "../config";
15
+ import { theme } from "../modes/interactive/theme/theme";
15
16
 
16
17
  /**
17
18
  * Detect if we're running as a Bun compiled binary.
@@ -142,7 +143,7 @@ async function updateViaBun(): Promise<void> {
142
143
 
143
144
  try {
144
145
  execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
145
- console.log(chalk.green("\n Update complete"));
146
+ console.log(chalk.green(`\n${theme.status.success} Update complete`));
146
147
  } catch {
147
148
  throw new Error("bun update failed");
148
149
  }
@@ -192,7 +193,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
192
193
  // Clean up backup
193
194
  unlinkSync(backupPath);
194
195
 
195
- console.log(chalk.green(`\n Updated to ${release.version}`));
196
+ console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
196
197
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
197
198
  } catch (err) {
198
199
  // Restore from backup if possible
@@ -224,7 +225,7 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
224
225
  const comparison = compareVersions(release.version, VERSION);
225
226
 
226
227
  if (comparison <= 0 && !opts.force) {
227
- console.log(chalk.green("✓ Already up to date"));
228
+ console.log(chalk.green(`${theme.status.success} Already up to date`));
228
229
  return;
229
230
  }
230
231
 
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
- import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
17
+ import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
18
18
  import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
19
19
  import type { Rule } from "../capability/rule";
20
20
  import { getAuthPath } from "../config";
@@ -479,10 +479,11 @@ export class AgentSession {
479
479
  }
480
480
 
481
481
  /**
482
- * Remove all listeners and disconnect from agent.
482
+ * Remove all listeners, flush pending writes, and disconnect from agent.
483
483
  * Call this when completely done with the session.
484
484
  */
485
- dispose(): void {
485
+ async dispose(): Promise<void> {
486
+ await this.sessionManager.flush();
486
487
  this._disconnectFromAgent();
487
488
  this._eventListeners = [];
488
489
  }
@@ -841,6 +842,7 @@ export class AgentSession {
841
842
  this._disconnectFromAgent();
842
843
  await this.abort();
843
844
  this.agent.reset();
845
+ await this.sessionManager.flush();
844
846
  this.sessionManager.newSession(options);
845
847
  this._queuedMessages = [];
846
848
  this._reconnectToAgent();
@@ -1636,8 +1638,11 @@ export class AgentSession {
1636
1638
  await this.abort();
1637
1639
  this._queuedMessages = [];
1638
1640
 
1641
+ // Flush pending writes before switching
1642
+ await this.sessionManager.flush();
1643
+
1639
1644
  // Set new session
1640
- this.sessionManager.setSessionFile(sessionPath);
1645
+ await this.sessionManager.setSessionFile(sessionPath);
1641
1646
 
1642
1647
  // Reload messages
1643
1648
  const sessionContext = this.sessionManager.buildSessionContext();
@@ -1714,6 +1719,9 @@ export class AgentSession {
1714
1719
  skipConversationRestore = result?.skipConversationRestore ?? false;
1715
1720
  }
1716
1721
 
1722
+ // Flush pending writes before branching
1723
+ await this.sessionManager.flush();
1724
+
1717
1725
  if (!selectedEntry.parentId) {
1718
1726
  this.sessionManager.newSession();
1719
1727
  } else {
@@ -1955,6 +1963,14 @@ export class AgentSession {
1955
1963
  let totalCacheWrite = 0;
1956
1964
  let totalCost = 0;
1957
1965
 
1966
+ const getTaskToolUsage = (details: unknown): Usage | undefined => {
1967
+ if (!details || typeof details !== "object") return undefined;
1968
+ const record = details as Record<string, unknown>;
1969
+ const usage = record.usage;
1970
+ if (!usage || typeof usage !== "object") return undefined;
1971
+ return usage as Usage;
1972
+ };
1973
+
1958
1974
  for (const message of state.messages) {
1959
1975
  if (message.role === "assistant") {
1960
1976
  const assistantMsg = message as AssistantMessage;
@@ -1965,6 +1981,17 @@ export class AgentSession {
1965
1981
  totalCacheWrite += assistantMsg.usage.cacheWrite;
1966
1982
  totalCost += assistantMsg.usage.cost.total;
1967
1983
  }
1984
+
1985
+ if (message.role === "toolResult" && message.toolName === "task") {
1986
+ const usage = getTaskToolUsage(message.details);
1987
+ if (usage) {
1988
+ totalInput += usage.input;
1989
+ totalOutput += usage.output;
1990
+ totalCacheRead += usage.cacheRead;
1991
+ totalCacheWrite += usage.cacheWrite;
1992
+ totalCost += usage.cost.total;
1993
+ }
1994
+ }
1968
1995
  }
1969
1996
 
1970
1997
  return {
@@ -8,6 +8,8 @@
8
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
9
  import type { Model } from "@oh-my-pi/pi-ai";
10
10
  import { completeSimple } from "@oh-my-pi/pi-ai";
11
+ import branchSummaryPrompt from "../../prompts/branch-summary.md" with { type: "text" };
12
+ import branchSummaryPreamble from "../../prompts/branch-summary-preamble.md" with { type: "text" };
11
13
  import {
12
14
  convertToLlm,
13
15
  createBranchSummaryMessage,
@@ -235,39 +237,9 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
235
237
  // Summary Generation
236
238
  // ============================================================================
237
239
 
238
- const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
239
- Summary of that exploration:
240
+ const BRANCH_SUMMARY_PREAMBLE = branchSummaryPreamble;
240
241
 
241
- `;
242
-
243
- const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
244
-
245
- Use this EXACT format:
246
-
247
- ## Goal
248
- [What was the user trying to accomplish in this branch?]
249
-
250
- ## Constraints & Preferences
251
- - [Any constraints, preferences, or requirements mentioned]
252
- - [Or "(none)" if none were mentioned]
253
-
254
- ## Progress
255
- ### Done
256
- - [x] [Completed tasks/changes]
257
-
258
- ### In Progress
259
- - [ ] [Work that was started but not finished]
260
-
261
- ### Blocked
262
- - [Issues preventing progress, if any]
263
-
264
- ## Key Decisions
265
- - **[Decision]**: [Brief rationale]
266
-
267
- ## Next Steps
268
- 1. [What should happen next to continue this work]
269
-
270
- Keep each section concise. Preserve exact file paths, function names, and error messages.`;
242
+ const BRANCH_SUMMARY_PROMPT = branchSummaryPrompt;
271
243
 
272
244
  /**
273
245
  * Generate a summary of abandoned branch entries.