@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +89 -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/tool-wrapper.ts +0 -1
  15. package/src/core/hooks/types.ts +2 -2
  16. package/src/core/plugins/doctor.ts +9 -1
  17. package/src/core/sdk.ts +2 -1
  18. package/src/core/session-manager.ts +518 -40
  19. package/src/core/settings-manager.ts +174 -0
  20. package/src/core/system-prompt.ts +9 -14
  21. package/src/core/title-generator.ts +2 -8
  22. package/src/core/tools/ask.ts +19 -37
  23. package/src/core/tools/bash.ts +2 -37
  24. package/src/core/tools/edit.ts +2 -9
  25. package/src/core/tools/exa/render.ts +52 -48
  26. package/src/core/tools/find.ts +10 -8
  27. package/src/core/tools/grep.ts +45 -17
  28. package/src/core/tools/ls.ts +22 -2
  29. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  30. package/src/core/tools/lsp/clients/index.ts +49 -0
  31. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  32. package/src/core/tools/lsp/config.ts +3 -0
  33. package/src/core/tools/lsp/index.ts +107 -55
  34. package/src/core/tools/lsp/render.ts +192 -79
  35. package/src/core/tools/lsp/types.ts +27 -0
  36. package/src/core/tools/lsp/utils.ts +62 -22
  37. package/src/core/tools/notebook.ts +9 -1
  38. package/src/core/tools/output.ts +37 -14
  39. package/src/core/tools/read.ts +349 -34
  40. package/src/core/tools/renderers.ts +290 -89
  41. package/src/core/tools/review.ts +12 -5
  42. package/src/core/tools/task/agents.ts +5 -5
  43. package/src/core/tools/task/commands.ts +3 -3
  44. package/src/core/tools/task/executor.ts +33 -1
  45. package/src/core/tools/task/index.ts +93 -6
  46. package/src/core/tools/task/render.ts +147 -66
  47. package/src/core/tools/task/types.ts +14 -9
  48. package/src/core/tools/web-fetch.ts +242 -103
  49. package/src/core/tools/web-search/index.ts +64 -20
  50. package/src/core/tools/web-search/providers/exa.ts +68 -172
  51. package/src/core/tools/web-search/render.ts +264 -74
  52. package/src/core/tools/write.ts +2 -8
  53. package/src/main.ts +10 -6
  54. package/src/modes/cleanup.ts +23 -0
  55. package/src/modes/index.ts +9 -4
  56. package/src/modes/interactive/components/bash-execution.ts +6 -3
  57. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  58. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  60. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  61. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  62. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  63. package/src/modes/interactive/components/hook-editor.ts +1 -0
  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 +172 -223
  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 +99 -13
  84. package/src/modes/interactive/theme/dark.json +3 -2
  85. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  86. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  87. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  88. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  89. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  90. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  91. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  92. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  93. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  94. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  95. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  96. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  97. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  98. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  99. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  100. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  101. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  102. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  103. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  104. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  105. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  106. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  107. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  108. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  109. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  110. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  111. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  112. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  113. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  114. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  115. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  116. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  117. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  118. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  119. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  120. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  121. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  122. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  123. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  124. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  125. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  126. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  127. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  128. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  129. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  130. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  131. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  132. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  133. package/src/modes/interactive/theme/defaults/index.ts +195 -0
  134. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  135. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  136. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  137. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  138. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  139. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  140. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  141. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  142. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  143. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  144. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  145. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  146. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  147. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  148. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  149. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  150. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  151. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  152. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  154. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  155. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  156. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  157. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  158. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  159. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  160. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  161. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  162. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  163. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  166. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  167. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  168. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  169. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  170. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  171. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  172. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  173. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  174. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  175. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  176. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  177. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  178. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  179. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  180. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  181. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  182. package/src/modes/interactive/theme/light.json +3 -2
  183. package/src/modes/interactive/theme/theme-schema.json +120 -4
  184. package/src/modes/interactive/theme/theme.ts +1228 -14
  185. package/src/prompts/branch-summary-preamble.md +3 -0
  186. package/src/prompts/branch-summary.md +28 -0
  187. package/src/prompts/compaction-summary.md +34 -0
  188. package/src/prompts/compaction-turn-prefix.md +16 -0
  189. package/src/prompts/compaction-update-summary.md +41 -0
  190. package/src/prompts/init.md +30 -0
  191. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  192. package/src/prompts/summarization-system.md +3 -0
  193. package/src/prompts/system-prompt.md +27 -0
  194. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  195. package/src/prompts/title-system.md +8 -0
  196. package/src/prompts/tools/ask.md +24 -0
  197. package/src/prompts/tools/bash.md +23 -0
  198. package/src/prompts/tools/edit.md +9 -0
  199. package/src/prompts/tools/find.md +6 -0
  200. package/src/prompts/tools/grep.md +12 -0
  201. package/src/prompts/tools/lsp.md +14 -0
  202. package/src/prompts/tools/output.md +23 -0
  203. package/src/prompts/tools/read.md +25 -0
  204. package/src/prompts/tools/web-fetch.md +8 -0
  205. package/src/prompts/tools/web-search.md +10 -0
  206. package/src/prompts/tools/write.md +10 -0
  207. package/src/commands/init.md +0 -20
  208. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  209. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  210. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  211. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  212. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  213. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
@@ -438,7 +438,11 @@ class TreeList implements Component {
438
438
  // Build prefix with gutters at their correct positions
439
439
  // Each gutter has a position (displayIndent where its connector was shown)
440
440
  const connector =
441
- flatNode.showConnector && !flatNode.isVirtualRootChild ? (flatNode.isLast ? "└─ " : "├─ ") : "";
441
+ flatNode.showConnector && !flatNode.isVirtualRootChild
442
+ ? flatNode.isLast
443
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal} `
444
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal} `
445
+ : "";
442
446
  const connectorPosition = connector ? displayIndent - 1 : -1;
443
447
 
444
448
  // Build prefix char by char, placing gutters and connector at their positions
@@ -452,16 +456,16 @@ class TreeList implements Component {
452
456
  const gutter = flatNode.gutters.find((g) => g.position === level);
453
457
  if (gutter) {
454
458
  if (posInLevel === 0) {
455
- prefixChars.push(gutter.show ? "│" : " ");
459
+ prefixChars.push(gutter.show ? theme.boxSharp.vertical : " ");
456
460
  } else {
457
461
  prefixChars.push(" ");
458
462
  }
459
463
  } else if (connector && level === connectorPosition) {
460
464
  // Connector at this level
461
465
  if (posInLevel === 0) {
462
- prefixChars.push(flatNode.isLast ? "└" : "├");
466
+ prefixChars.push(flatNode.isLast ? theme.boxSharp.bottomLeft : theme.boxSharp.teeRight);
463
467
  } else if (posInLevel === 1) {
464
- prefixChars.push("─");
468
+ prefixChars.push(theme.boxSharp.horizontal);
465
469
  } else {
466
470
  prefixChars.push(" ");
467
471
  }
@@ -473,7 +477,7 @@ class TreeList implements Component {
473
477
 
474
478
  // Active path marker - shown right before the entry text
475
479
  const isOnActivePath = this.activePathIds.has(entry.id);
476
- const pathMarker = isOnActivePath ? theme.fg("accent", "• ") : "";
480
+ const pathMarker = isOnActivePath ? theme.fg("accent", `${theme.md.bullet} `) : "";
477
481
 
478
482
  const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
479
483
  const content = this.getEntryDisplayText(flatNode.node, isSelected);
@@ -816,7 +820,14 @@ export class TreeSelectorComponent extends Container {
816
820
  this.addChild(new DynamicBorder());
817
821
  this.addChild(new Text(theme.bold(" Session Tree"), 1, 0));
818
822
  this.addChild(
819
- new TruncatedText(theme.fg("muted", " ↑/↓: move. ←/→: page. l: label. ^O/⇧^O: filter. Type to search"), 0, 0),
823
+ new TruncatedText(
824
+ theme.fg(
825
+ "muted",
826
+ " Up/Down: move. Left/Right: page. l: label. Ctrl+O/Shift+Ctrl+O: filter. Type to search",
827
+ ),
828
+ 0,
829
+ 0,
830
+ ),
820
831
  );
821
832
  this.addChild(new SearchLine(this.treeList));
822
833
  this.addChild(new DynamicBorder());
@@ -38,13 +38,13 @@ export class TtsrNotificationComponent extends Container {
38
38
  private rebuild(): void {
39
39
  this.box.clear();
40
40
 
41
- // Build header: Injecting <bold>rule-name</bold> ↩
41
+ // Build header: warning symbol + rule name + rewind icon
42
42
  const ruleNames = this.rules.map((r) => theme.bold(r.name)).join(", ");
43
43
  const label = this.rules.length === 1 ? "rule" : "rules";
44
- const header = `\u26A0 Injecting ${label}: ${ruleNames}`;
44
+ const header = `${theme.icon.warning} Injecting ${label}: ${ruleNames}`;
45
45
 
46
46
  // Create header with rewind icon on the right
47
- const rewindIcon = "\u21A9"; // ↩
47
+ const rewindIcon = theme.icon.rewind;
48
48
 
49
49
  this.box.addChild(new Text(`${header} ${rewindIcon}`, 0, 0));
50
50
 
@@ -59,7 +59,7 @@ export class TtsrNotificationComponent extends Container {
59
59
  // Truncate to first 2 lines
60
60
  const lines = displayText.split("\n");
61
61
  if (lines.length > 2) {
62
- displayText = `${lines.slice(0, 2).join("\n")}...`;
62
+ displayText = `${lines.slice(0, 2).join("\n")}${theme.format.ellipsis}`;
63
63
  }
64
64
  }
65
65
 
@@ -1,4 +1,4 @@
1
- import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import { APP_NAME } from "../../../config";
3
3
  import { theme } from "../theme/theme";
4
4
 
@@ -80,7 +80,7 @@ export class WelcomeComponent implements Component {
80
80
 
81
81
  // Right column separator
82
82
  const separatorWidth = rightCol - 2; // padding on each side
83
- const separator = ` ${theme.fg("dim", "─".repeat(separatorWidth))}`;
83
+ const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
84
84
 
85
85
  // Recent sessions content
86
86
  const sessionLines: string[] = [];
@@ -89,7 +89,7 @@ export class WelcomeComponent implements Component {
89
89
  } else {
90
90
  for (const session of this.recentSessions.slice(0, 3)) {
91
91
  sessionLines.push(
92
- ` ${theme.fg("dim", "▪ ")}${theme.fg("muted", session.name)}${theme.fg("dim", ` (${session.timeAgo})`)}`,
92
+ ` ${theme.fg("dim", `${theme.md.bullet} `)}${theme.fg("muted", session.name)}${theme.fg("dim", ` (${session.timeAgo})`)}`,
93
93
  );
94
94
  }
95
95
  }
@@ -102,10 +102,10 @@ export class WelcomeComponent implements Component {
102
102
  for (const server of this.lspServers) {
103
103
  const icon =
104
104
  server.status === "ready"
105
- ? theme.fg("success", "")
105
+ ? theme.styledSymbol("status.success", "success")
106
106
  : server.status === "connecting"
107
- ? theme.fg("warning", "")
108
- : theme.fg("error", "");
107
+ ? theme.styledSymbol("status.disabled", "warning")
108
+ : theme.styledSymbol("status.error", "error");
109
109
  const exts = server.fileTypes.slice(0, 3).join(" ");
110
110
  lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
111
111
  }
@@ -127,21 +127,24 @@ export class WelcomeComponent implements Component {
127
127
  ];
128
128
 
129
129
  // Border characters (dim)
130
- const h = theme.fg("dim", "─");
131
- const v = theme.fg("dim", "│");
132
- const tl = theme.fg("dim", "╭");
133
- const tr = theme.fg("dim", "╮");
134
- const bl = theme.fg("dim", "╰");
135
- const br = theme.fg("dim", "╯");
130
+ const hChar = theme.boxRound.horizontal;
131
+ const h = theme.fg("dim", hChar);
132
+ const v = theme.fg("dim", theme.boxRound.vertical);
133
+ const tl = theme.fg("dim", theme.boxRound.topLeft);
134
+ const tr = theme.fg("dim", theme.boxRound.topRight);
135
+ const bl = theme.fg("dim", theme.boxRound.bottomLeft);
136
+ const br = theme.fg("dim", theme.boxRound.bottomRight);
136
137
 
137
138
  const lines: string[] = [];
138
139
 
139
140
  // Top border with embedded title
140
141
  const title = ` ${APP_NAME} v${this.version} `;
141
- const titleStyled = theme.fg("dim", "───") + theme.fg("muted", title);
142
- const titleVisLen = 3 + title.length;
142
+ const titlePrefixRaw = hChar.repeat(3);
143
+ const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
144
+ const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
143
145
  const afterTitle = boxWidth - 2 - titleVisLen;
144
- lines.push(tl + titleStyled + h.repeat(Math.max(0, afterTitle)) + tr);
146
+ const afterTitleText = afterTitle > 0 ? theme.fg("dim", hChar.repeat(afterTitle)) : "";
147
+ lines.push(tl + titleStyled + afterTitleText + tr);
145
148
 
146
149
  // Content rows
147
150
  const maxRows = Math.max(leftLines.length, rightLines.length);
@@ -152,7 +155,7 @@ export class WelcomeComponent implements Component {
152
155
  }
153
156
 
154
157
  // Bottom border
155
- lines.push(bl + h.repeat(leftCol) + theme.fg("dim", "┴") + h.repeat(rightCol) + br);
158
+ lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
156
159
 
157
160
  return lines;
158
161
  }
@@ -160,7 +163,9 @@ export class WelcomeComponent implements Component {
160
163
  /** Center text within a given width */
161
164
  private centerText(text: string, width: number): string {
162
165
  const visLen = visibleWidth(text);
163
- if (visLen >= width) return text;
166
+ if (visLen >= width) {
167
+ return truncateToWidth(text, width, theme.format.ellipsis);
168
+ }
164
169
  const leftPad = Math.floor((width - visLen) / 2);
165
170
  const rightPad = width - visLen - leftPad;
166
171
  return " ".repeat(leftPad) + text + " ".repeat(rightPad);
@@ -200,6 +205,9 @@ export class WelcomeComponent implements Component {
200
205
  private fitToWidth(str: string, width: number): string {
201
206
  const visLen = visibleWidth(str);
202
207
  if (visLen > width) {
208
+ const ellipsis = theme.format.ellipsis;
209
+ const ellipsisWidth = visibleWidth(ellipsis);
210
+ const maxWidth = Math.max(0, width - ellipsisWidth);
203
211
  let truncated = "";
204
212
  let currentWidth = 0;
205
213
  let inEscape = false;
@@ -208,12 +216,12 @@ export class WelcomeComponent implements Component {
208
216
  if (inEscape) {
209
217
  truncated += char;
210
218
  if (char === "m") inEscape = false;
211
- } else if (currentWidth < width - 1) {
219
+ } else if (currentWidth < maxWidth) {
212
220
  truncated += char;
213
221
  currentWidth++;
214
222
  }
215
223
  }
216
- return `${truncated}…`;
224
+ return `${truncated}${ellipsis}`;
217
225
  }
218
226
  return str + " ".repeat(width - visLen);
219
227
  }
@@ -34,6 +34,7 @@ import type { TruncationResult } from "../../core/tools/truncate";
34
34
  import { disableProvider, enableProvider } from "../../discovery";
35
35
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
36
36
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
37
+ import { registerAsyncCleanup } from "../cleanup";
37
38
  import { ArminComponent } from "./components/armin";
38
39
  import { AssistantMessageComponent } from "./components/assistant-message";
39
40
  import { BashExecutionComponent } from "./components/bash-execution";
@@ -62,7 +63,9 @@ import {
62
63
  getAvailableThemes,
63
64
  getEditorTheme,
64
65
  getMarkdownTheme,
66
+ getSymbolTheme,
65
67
  onThemeChange,
68
+ setSymbolPreset,
66
69
  setTheme,
67
70
  type Theme,
68
71
  theme,
@@ -115,6 +118,9 @@ export class InteractiveMode {
115
118
  // Agent subscription unsubscribe function
116
119
  private unsubscribe?: () => void;
117
120
 
121
+ // Signal cleanup unsubscribe function (for SIGINT/SIGTERM flush)
122
+ private cleanupUnsubscribe?: () => void;
123
+
118
124
  // Track if editor is in bash mode (text starts with !)
119
125
  private isBashMode = false;
120
126
 
@@ -174,6 +180,7 @@ export class InteractiveMode {
174
180
  this.pendingMessagesContainer = new Container();
175
181
  this.statusContainer = new Container();
176
182
  this.editor = new CustomEditor(getEditorTheme());
183
+ this.editor.setUseTerminalCursor(true);
177
184
  this.editorContainer = new Container();
178
185
  this.editorContainer.addChild(this.editor);
179
186
  this.statusLine = new StatusLineComponent(session);
@@ -198,6 +205,7 @@ export class InteractiveMode {
198
205
  { name: "new", description: "Start a new session" },
199
206
  { name: "compact", description: "Manually compact the session context" },
200
207
  { name: "resume", description: "Resume a different session" },
208
+ { name: "exit", description: "Exit the application" },
201
209
  ];
202
210
 
203
211
  // Load hide thinking block setting
@@ -233,6 +241,9 @@ export class InteractiveMode {
233
241
  async init(): Promise<void> {
234
242
  if (this.isInitialized) return;
235
243
 
244
+ // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
245
+ this.cleanupUnsubscribe = registerAsyncCleanup(() => this.sessionManager.flush());
246
+
236
247
  // Get current model info for welcome screen
237
248
  const modelName = this.session.model?.name ?? "Unknown";
238
249
  const providerName = this.session.model?.provider ?? "Unknown";
@@ -403,7 +414,9 @@ export class InteractiveMode {
403
414
  this.pendingTools.clear();
404
415
 
405
416
  this.chatContainer.addChild(new Spacer(1));
406
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
417
+ this.chatContainer.addChild(
418
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
419
+ );
407
420
  this.ui.requestRender();
408
421
 
409
422
  return { cancelled: false };
@@ -838,6 +851,11 @@ export class InteractiveMode {
838
851
  this.editor.setText("");
839
852
  return;
840
853
  }
854
+ if (text === "/exit") {
855
+ this.editor.setText("");
856
+ void this.shutdown();
857
+ return;
858
+ }
841
859
 
842
860
  // Handle bash command
843
861
  if (text.startsWith("!")) {
@@ -908,9 +926,9 @@ export class InteractiveMode {
908
926
  const registry = this.session.modelRegistry;
909
927
  const smolModel = this.settingsManager.getModelRole("smol");
910
928
  generateSessionTitle(text, registry, smolModel)
911
- .then((title) => {
929
+ .then(async (title) => {
912
930
  if (title) {
913
- this.sessionManager.setSessionTitle(title);
931
+ await this.sessionManager.setSessionTitle(title);
914
932
  setTerminalTitle(`omp: ${title}`);
915
933
  }
916
934
  })
@@ -951,7 +969,8 @@ export class InteractiveMode {
951
969
  this.ui,
952
970
  (spinner) => theme.fg("accent", spinner),
953
971
  (text) => theme.fg("muted", text),
954
- "Working... (esc to interrupt)",
972
+ `Working${theme.format.ellipsis} (esc to interrupt)`,
973
+ getSymbolTheme().spinnerFrames,
955
974
  );
956
975
  this.statusContainer.addChild(this.loadingAnimation);
957
976
  this.ui.requestRender();
@@ -1118,7 +1137,8 @@ export class InteractiveMode {
1118
1137
  this.ui,
1119
1138
  (spinner) => theme.fg("accent", spinner),
1120
1139
  (text) => theme.fg("muted", text),
1121
- `${reasonText}Auto-compacting... (esc to cancel)`,
1140
+ `${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
1141
+ getSymbolTheme().spinnerFrames,
1122
1142
  );
1123
1143
  this.statusContainer.addChild(this.autoCompactionLoader);
1124
1144
  this.ui.requestRender();
@@ -1173,7 +1193,8 @@ export class InteractiveMode {
1173
1193
  this.ui,
1174
1194
  (spinner) => theme.fg("warning", spinner),
1175
1195
  (text) => theme.fg("muted", text),
1176
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (esc to cancel)`,
1196
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
1197
+ getSymbolTheme().spinnerFrames,
1177
1198
  );
1178
1199
  this.statusContainer.addChild(this.retryLoader);
1179
1200
  this.ui.requestRender();
@@ -1287,7 +1308,7 @@ export class InteractiveMode {
1287
1308
  case "fileMention": {
1288
1309
  // Render compact file mention display
1289
1310
  for (const file of message.files) {
1290
- const text = `${theme.fg("dim", "⎿ ")}${theme.fg("muted", "Read")} ${theme.fg("accent", file.path)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
1311
+ const text = `${theme.fg("dim", `${theme.tree.hook} `)}${theme.fg("muted", "Read")} ${theme.fg("accent", file.path)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
1291
1312
  this.chatContainer.addChild(new Text(text, 0, 0));
1292
1313
  }
1293
1314
  break;
@@ -1434,6 +1455,9 @@ export class InteractiveMode {
1434
1455
  * Emits shutdown event to hooks and tools, then exits.
1435
1456
  */
1436
1457
  private async shutdown(): Promise<void> {
1458
+ // Flush pending session writes before shutdown
1459
+ await this.sessionManager.flush();
1460
+
1437
1461
  // Emit shutdown event to hooks
1438
1462
  const hookRunner = this.session.hookRunner;
1439
1463
  if (hookRunner?.hasHandlers("session_shutdown")) {
@@ -1714,11 +1738,26 @@ export class InteractiveMode {
1714
1738
  this.ui.requestRender();
1715
1739
  }
1716
1740
  },
1741
+ onStatusLinePreview: (settings) => {
1742
+ // Update status line with preview settings
1743
+ const currentSettings = this.settingsManager.getStatusLineSettings();
1744
+ this.statusLine.updateSettings({ ...currentSettings, ...settings });
1745
+ this.updateEditorTopBorder();
1746
+ this.ui.requestRender();
1747
+ },
1748
+ getStatusLinePreview: () => {
1749
+ // Return the rendered status line for inline preview
1750
+ const width = this.ui.getWidth();
1751
+ return this.statusLine.getTopBorder(width).content;
1752
+ },
1717
1753
  onPluginsChanged: () => {
1718
1754
  this.ui.requestRender();
1719
1755
  },
1720
1756
  onCancel: () => {
1721
1757
  done();
1758
+ // Restore status line to saved settings
1759
+ this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
1760
+ this.updateEditorTopBorder();
1722
1761
  this.ui.requestRender();
1723
1762
  },
1724
1763
  },
@@ -1797,12 +1836,40 @@ export class InteractiveMode {
1797
1836
  break;
1798
1837
  case "theme": {
1799
1838
  const result = setTheme(value as string, true);
1839
+ this.statusLine.invalidate();
1840
+ this.updateEditorTopBorder();
1800
1841
  this.ui.invalidate();
1801
1842
  if (!result.success) {
1802
1843
  this.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
1803
1844
  }
1804
1845
  break;
1805
1846
  }
1847
+ case "symbolPreset": {
1848
+ setSymbolPreset(value as "unicode" | "nerd" | "ascii");
1849
+ this.statusLine.invalidate();
1850
+ this.updateEditorTopBorder();
1851
+ this.ui.invalidate();
1852
+ break;
1853
+ }
1854
+ case "statusLinePreset":
1855
+ case "statusLineSeparator":
1856
+ case "statusLineShowHooks":
1857
+ case "statusLineSegments":
1858
+ case "statusLineModelThinking":
1859
+ case "statusLinePathAbbreviate":
1860
+ case "statusLinePathMaxLength":
1861
+ case "statusLinePathStripWorkPrefix":
1862
+ case "statusLineGitShowBranch":
1863
+ case "statusLineGitShowStaged":
1864
+ case "statusLineGitShowUnstaged":
1865
+ case "statusLineGitShowUntracked":
1866
+ case "statusLineTimeFormat":
1867
+ case "statusLineTimeShowSeconds": {
1868
+ this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
1869
+ this.updateEditorTopBorder();
1870
+ this.ui.requestRender();
1871
+ break;
1872
+ }
1806
1873
 
1807
1874
  // All other settings are handled by the definitions (get/set on SettingsManager)
1808
1875
  // No additional side effects needed
@@ -1930,6 +1997,7 @@ export class InteractiveMode {
1930
1997
  (spinner) => theme.fg("accent", spinner),
1931
1998
  (text) => theme.fg("muted", text),
1932
1999
  "Summarizing branch... (esc to cancel)",
2000
+ getSymbolTheme().spinnerFrames,
1933
2001
  );
1934
2002
  this.statusContainer.addChild(summaryLoader);
1935
2003
  this.ui.requestRender();
@@ -2092,7 +2160,11 @@ export class InteractiveMode {
2092
2160
  this.session.modelRegistry.refresh();
2093
2161
  this.chatContainer.addChild(new Spacer(1));
2094
2162
  this.chatContainer.addChild(
2095
- new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0),
2163
+ new Text(
2164
+ theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
2165
+ 1,
2166
+ 0,
2167
+ ),
2096
2168
  );
2097
2169
  this.chatContainer.addChild(
2098
2170
  new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0),
@@ -2108,7 +2180,11 @@ export class InteractiveMode {
2108
2180
  this.session.modelRegistry.refresh();
2109
2181
  this.chatContainer.addChild(new Spacer(1));
2110
2182
  this.chatContainer.addChild(
2111
- new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),
2183
+ new Text(
2184
+ theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
2185
+ 1,
2186
+ 0,
2187
+ ),
2112
2188
  );
2113
2189
  this.chatContainer.addChild(
2114
2190
  new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0),
@@ -2346,8 +2422,8 @@ export class InteractiveMode {
2346
2422
 
2347
2423
  this.chatContainer.addChild(new Spacer(1));
2348
2424
  this.chatContainer.addChild(new DynamicBorder());
2349
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
2350
- this.ui.addChild(new Spacer(1));
2425
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
2426
+ this.chatContainer.addChild(new Spacer(1));
2351
2427
  this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
2352
2428
  this.chatContainer.addChild(new DynamicBorder());
2353
2429
  this.ui.requestRender();
@@ -2420,7 +2496,9 @@ export class InteractiveMode {
2420
2496
  this.pendingTools.clear();
2421
2497
 
2422
2498
  this.chatContainer.addChild(new Spacer(1));
2423
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
2499
+ this.chatContainer.addChild(
2500
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
2501
+ );
2424
2502
  this.ui.requestRender();
2425
2503
  }
2426
2504
 
@@ -2451,7 +2529,11 @@ export class InteractiveMode {
2451
2529
 
2452
2530
  this.chatContainer.addChild(new Spacer(1));
2453
2531
  this.chatContainer.addChild(
2454
- new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1),
2532
+ new Text(
2533
+ `${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
2534
+ 1,
2535
+ 1,
2536
+ ),
2455
2537
  );
2456
2538
  this.ui.requestRender();
2457
2539
  }
@@ -2537,6 +2619,7 @@ export class InteractiveMode {
2537
2619
  (spinner) => theme.fg("accent", spinner),
2538
2620
  (text) => theme.fg("muted", text),
2539
2621
  label,
2622
+ getSymbolTheme().spinnerFrames,
2540
2623
  );
2541
2624
  this.statusContainer.addChild(compactingLoader);
2542
2625
  this.ui.requestRender();
@@ -2576,6 +2659,9 @@ export class InteractiveMode {
2576
2659
  if (this.unsubscribe) {
2577
2660
  this.unsubscribe();
2578
2661
  }
2662
+ if (this.cleanupUnsubscribe) {
2663
+ this.cleanupUnsubscribe();
2664
+ }
2579
2665
  if (this.isInitialized) {
2580
2666
  this.ui.stop();
2581
2667
  this.isInitialized = false;
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
2
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
3
3
  "name": "dark",
4
4
  "vars": {
5
5
  "cyan": "#0088fa",
@@ -91,7 +91,8 @@
91
91
  "statusLineDirty": 178,
92
92
  "statusLineUntracked": 39,
93
93
  "statusLineOutput": 205,
94
- "statusLineCost": 205
94
+ "statusLineCost": 205,
95
+ "statusLineSubagents": "accent"
95
96
  },
96
97
  "export": {
97
98
  "pageBg": "#18181e",
@@ -0,0 +1,99 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
3
+ "name": "alabaster",
4
+ "vars": {
5
+ "alabaster": "#fdfcfb",
6
+ "chiseledBlack": "#1a1c20",
7
+ "shadowGray": "#505860",
8
+ "carvedDepth": "#6a7080",
9
+ "subtleShadow": "#e8e8ec",
10
+ "lightShadow": "#f0f0f4",
11
+ "accent": "#404850",
12
+ "warmShadow": "#8a7060",
13
+ "selectedBg": "#f0eff0",
14
+ "userMsgBg": "#f5f4f5",
15
+ "toolPendingBg": "#f3f3f8",
16
+ "toolSuccessBg": "#f4f5f4",
17
+ "toolErrorBg": "#f8f3f3",
18
+ "customMsgBg": "#f6f4f7"
19
+ },
20
+ "colors": {
21
+ "accent": "shadowGray",
22
+ "border": "subtleShadow",
23
+ "borderAccent": "shadowGray",
24
+ "borderMuted": "lightShadow",
25
+ "success": "#405840",
26
+ "error": "#704040",
27
+ "warning": "#806850",
28
+ "muted": "carvedDepth",
29
+ "dim": "#909098",
30
+ "text": "",
31
+ "thinkingText": "carvedDepth",
32
+
33
+ "selectedBg": "selectedBg",
34
+ "userMessageBg": "userMsgBg",
35
+ "userMessageText": "",
36
+ "customMessageBg": "customMsgBg",
37
+ "customMessageText": "",
38
+ "customMessageLabel": "shadowGray",
39
+ "toolPendingBg": "toolPendingBg",
40
+ "toolSuccessBg": "toolSuccessBg",
41
+ "toolErrorBg": "toolErrorBg",
42
+ "toolTitle": "",
43
+ "toolOutput": "carvedDepth",
44
+
45
+ "mdHeading": "chiseledBlack",
46
+ "mdLink": "shadowGray",
47
+ "mdLinkUrl": "carvedDepth",
48
+ "mdCode": "accent",
49
+ "mdCodeBlock": "shadowGray",
50
+ "mdCodeBlockBorder": "subtleShadow",
51
+ "mdQuote": "carvedDepth",
52
+ "mdQuoteBorder": "subtleShadow",
53
+ "mdHr": "subtleShadow",
54
+ "mdListBullet": "shadowGray",
55
+
56
+ "toolDiffAdded": "#405840",
57
+ "toolDiffRemoved": "#704040",
58
+ "toolDiffContext": "carvedDepth",
59
+
60
+ "syntaxComment": "#707880",
61
+ "syntaxKeyword": "#303840",
62
+ "syntaxFunction": "chiseledBlack",
63
+ "syntaxVariable": "chiseledBlack",
64
+ "syntaxString": "warmShadow",
65
+ "syntaxNumber": "shadowGray",
66
+ "syntaxType": "#303840",
67
+ "syntaxOperator": "#404850",
68
+ "syntaxPunctuation": "#505860",
69
+
70
+ "thinkingOff": "#c0c0c8",
71
+ "thinkingMinimal": "#a0a0a8",
72
+ "thinkingLow": "#808090",
73
+ "thinkingMedium": "#606870",
74
+ "thinkingHigh": "#505860",
75
+ "thinkingXhigh": "#303840",
76
+
77
+ "bashMode": "#405840",
78
+
79
+ "statusLineBg": "#ececf0",
80
+ "statusLineSep": "#a0a0a8",
81
+ "statusLineModel": "#505860",
82
+ "statusLinePath": "#404850",
83
+ "statusLineGitClean": "#305030",
84
+ "statusLineGitDirty": "#806030",
85
+ "statusLineContext": "#404860",
86
+ "statusLineSpend": "#405050",
87
+ "statusLineStaged": 28,
88
+ "statusLineDirty": 136,
89
+ "statusLineUntracked": 31,
90
+ "statusLineOutput": 133,
91
+ "statusLineCost": 133,
92
+ "statusLineSubagents": "shadowGray"
93
+ },
94
+ "export": {
95
+ "pageBg": "#fdfcfb",
96
+ "cardBg": "#ffffff",
97
+ "infoBg": "#faf9f8"
98
+ }
99
+ }