@oh-my-pi/pi-coding-agent 3.14.0 → 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 (148) hide show
  1. package/CHANGELOG.md +79 -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-message.ts +2 -2
  64. package/src/modes/interactive/components/hook-selector.ts +1 -1
  65. package/src/modes/interactive/components/model-selector.ts +22 -9
  66. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  67. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  68. package/src/modes/interactive/components/session-selector.ts +9 -6
  69. package/src/modes/interactive/components/settings-defs.ts +285 -1
  70. package/src/modes/interactive/components/settings-selector.ts +176 -3
  71. package/src/modes/interactive/components/status-line/index.ts +4 -0
  72. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  73. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  74. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  75. package/src/modes/interactive/components/status-line/types.ts +81 -0
  76. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  77. package/src/modes/interactive/components/status-line.ts +170 -223
  78. package/src/modes/interactive/components/tool-execution.ts +446 -211
  79. package/src/modes/interactive/components/tree-selector.ts +17 -6
  80. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  81. package/src/modes/interactive/components/welcome.ts +27 -19
  82. package/src/modes/interactive/interactive-mode.ts +98 -13
  83. package/src/modes/interactive/theme/dark.json +3 -2
  84. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  85. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  86. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  87. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  88. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  89. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  90. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  91. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  92. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  93. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  94. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  95. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  96. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  97. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  98. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  99. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  100. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  101. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  102. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  103. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  104. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  105. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  106. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  107. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  108. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  109. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  110. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  111. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  112. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  113. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  114. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  115. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  116. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  117. package/src/modes/interactive/theme/light.json +3 -2
  118. package/src/modes/interactive/theme/theme-schema.json +120 -4
  119. package/src/modes/interactive/theme/theme.ts +1228 -14
  120. package/src/prompts/branch-summary-preamble.md +3 -0
  121. package/src/prompts/branch-summary.md +28 -0
  122. package/src/prompts/compaction-summary.md +34 -0
  123. package/src/prompts/compaction-turn-prefix.md +16 -0
  124. package/src/prompts/compaction-update-summary.md +41 -0
  125. package/src/prompts/init.md +30 -0
  126. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  127. package/src/prompts/summarization-system.md +3 -0
  128. package/src/prompts/system-prompt.md +27 -0
  129. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  130. package/src/prompts/title-system.md +8 -0
  131. package/src/prompts/tools/ask.md +24 -0
  132. package/src/prompts/tools/bash.md +23 -0
  133. package/src/prompts/tools/edit.md +9 -0
  134. package/src/prompts/tools/find.md +6 -0
  135. package/src/prompts/tools/grep.md +12 -0
  136. package/src/prompts/tools/lsp.md +14 -0
  137. package/src/prompts/tools/output.md +23 -0
  138. package/src/prompts/tools/read.md +25 -0
  139. package/src/prompts/tools/web-fetch.md +8 -0
  140. package/src/prompts/tools/web-search.md +10 -0
  141. package/src/prompts/tools/write.md +10 -0
  142. package/src/commands/init.md +0 -20
  143. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  144. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  145. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  146. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  147. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  148. /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
 
@@ -198,6 +204,7 @@ export class InteractiveMode {
198
204
  { name: "new", description: "Start a new session" },
199
205
  { name: "compact", description: "Manually compact the session context" },
200
206
  { name: "resume", description: "Resume a different session" },
207
+ { name: "exit", description: "Exit the application" },
201
208
  ];
202
209
 
203
210
  // Load hide thinking block setting
@@ -233,6 +240,9 @@ export class InteractiveMode {
233
240
  async init(): Promise<void> {
234
241
  if (this.isInitialized) return;
235
242
 
243
+ // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
244
+ this.cleanupUnsubscribe = registerAsyncCleanup(() => this.sessionManager.flush());
245
+
236
246
  // Get current model info for welcome screen
237
247
  const modelName = this.session.model?.name ?? "Unknown";
238
248
  const providerName = this.session.model?.provider ?? "Unknown";
@@ -403,7 +413,9 @@ export class InteractiveMode {
403
413
  this.pendingTools.clear();
404
414
 
405
415
  this.chatContainer.addChild(new Spacer(1));
406
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
416
+ this.chatContainer.addChild(
417
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
418
+ );
407
419
  this.ui.requestRender();
408
420
 
409
421
  return { cancelled: false };
@@ -838,6 +850,11 @@ export class InteractiveMode {
838
850
  this.editor.setText("");
839
851
  return;
840
852
  }
853
+ if (text === "/exit") {
854
+ this.editor.setText("");
855
+ void this.shutdown();
856
+ return;
857
+ }
841
858
 
842
859
  // Handle bash command
843
860
  if (text.startsWith("!")) {
@@ -908,9 +925,9 @@ export class InteractiveMode {
908
925
  const registry = this.session.modelRegistry;
909
926
  const smolModel = this.settingsManager.getModelRole("smol");
910
927
  generateSessionTitle(text, registry, smolModel)
911
- .then((title) => {
928
+ .then(async (title) => {
912
929
  if (title) {
913
- this.sessionManager.setSessionTitle(title);
930
+ await this.sessionManager.setSessionTitle(title);
914
931
  setTerminalTitle(`omp: ${title}`);
915
932
  }
916
933
  })
@@ -951,7 +968,8 @@ export class InteractiveMode {
951
968
  this.ui,
952
969
  (spinner) => theme.fg("accent", spinner),
953
970
  (text) => theme.fg("muted", text),
954
- "Working... (esc to interrupt)",
971
+ `Working${theme.format.ellipsis} (esc to interrupt)`,
972
+ getSymbolTheme().spinnerFrames,
955
973
  );
956
974
  this.statusContainer.addChild(this.loadingAnimation);
957
975
  this.ui.requestRender();
@@ -1118,7 +1136,8 @@ export class InteractiveMode {
1118
1136
  this.ui,
1119
1137
  (spinner) => theme.fg("accent", spinner),
1120
1138
  (text) => theme.fg("muted", text),
1121
- `${reasonText}Auto-compacting... (esc to cancel)`,
1139
+ `${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
1140
+ getSymbolTheme().spinnerFrames,
1122
1141
  );
1123
1142
  this.statusContainer.addChild(this.autoCompactionLoader);
1124
1143
  this.ui.requestRender();
@@ -1173,7 +1192,8 @@ export class InteractiveMode {
1173
1192
  this.ui,
1174
1193
  (spinner) => theme.fg("warning", spinner),
1175
1194
  (text) => theme.fg("muted", text),
1176
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (esc to cancel)`,
1195
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
1196
+ getSymbolTheme().spinnerFrames,
1177
1197
  );
1178
1198
  this.statusContainer.addChild(this.retryLoader);
1179
1199
  this.ui.requestRender();
@@ -1287,7 +1307,7 @@ export class InteractiveMode {
1287
1307
  case "fileMention": {
1288
1308
  // Render compact file mention display
1289
1309
  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)`)}`;
1310
+ const text = `${theme.fg("dim", `${theme.tree.hook} `)}${theme.fg("muted", "Read")} ${theme.fg("accent", file.path)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
1291
1311
  this.chatContainer.addChild(new Text(text, 0, 0));
1292
1312
  }
1293
1313
  break;
@@ -1434,6 +1454,9 @@ export class InteractiveMode {
1434
1454
  * Emits shutdown event to hooks and tools, then exits.
1435
1455
  */
1436
1456
  private async shutdown(): Promise<void> {
1457
+ // Flush pending session writes before shutdown
1458
+ await this.sessionManager.flush();
1459
+
1437
1460
  // Emit shutdown event to hooks
1438
1461
  const hookRunner = this.session.hookRunner;
1439
1462
  if (hookRunner?.hasHandlers("session_shutdown")) {
@@ -1714,11 +1737,26 @@ export class InteractiveMode {
1714
1737
  this.ui.requestRender();
1715
1738
  }
1716
1739
  },
1740
+ onStatusLinePreview: (settings) => {
1741
+ // Update status line with preview settings
1742
+ const currentSettings = this.settingsManager.getStatusLineSettings();
1743
+ this.statusLine.updateSettings({ ...currentSettings, ...settings });
1744
+ this.updateEditorTopBorder();
1745
+ this.ui.requestRender();
1746
+ },
1747
+ getStatusLinePreview: () => {
1748
+ // Return the rendered status line for inline preview
1749
+ const width = this.ui.getWidth();
1750
+ return this.statusLine.getTopBorder(width).content;
1751
+ },
1717
1752
  onPluginsChanged: () => {
1718
1753
  this.ui.requestRender();
1719
1754
  },
1720
1755
  onCancel: () => {
1721
1756
  done();
1757
+ // Restore status line to saved settings
1758
+ this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
1759
+ this.updateEditorTopBorder();
1722
1760
  this.ui.requestRender();
1723
1761
  },
1724
1762
  },
@@ -1797,12 +1835,40 @@ export class InteractiveMode {
1797
1835
  break;
1798
1836
  case "theme": {
1799
1837
  const result = setTheme(value as string, true);
1838
+ this.statusLine.invalidate();
1839
+ this.updateEditorTopBorder();
1800
1840
  this.ui.invalidate();
1801
1841
  if (!result.success) {
1802
1842
  this.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
1803
1843
  }
1804
1844
  break;
1805
1845
  }
1846
+ case "symbolPreset": {
1847
+ setSymbolPreset(value as "unicode" | "nerd" | "ascii");
1848
+ this.statusLine.invalidate();
1849
+ this.updateEditorTopBorder();
1850
+ this.ui.invalidate();
1851
+ break;
1852
+ }
1853
+ case "statusLinePreset":
1854
+ case "statusLineSeparator":
1855
+ case "statusLineShowHooks":
1856
+ case "statusLineSegments":
1857
+ case "statusLineModelThinking":
1858
+ case "statusLinePathAbbreviate":
1859
+ case "statusLinePathMaxLength":
1860
+ case "statusLinePathStripWorkPrefix":
1861
+ case "statusLineGitShowBranch":
1862
+ case "statusLineGitShowStaged":
1863
+ case "statusLineGitShowUnstaged":
1864
+ case "statusLineGitShowUntracked":
1865
+ case "statusLineTimeFormat":
1866
+ case "statusLineTimeShowSeconds": {
1867
+ this.statusLine.updateSettings(this.settingsManager.getStatusLineSettings());
1868
+ this.updateEditorTopBorder();
1869
+ this.ui.requestRender();
1870
+ break;
1871
+ }
1806
1872
 
1807
1873
  // All other settings are handled by the definitions (get/set on SettingsManager)
1808
1874
  // No additional side effects needed
@@ -1930,6 +1996,7 @@ export class InteractiveMode {
1930
1996
  (spinner) => theme.fg("accent", spinner),
1931
1997
  (text) => theme.fg("muted", text),
1932
1998
  "Summarizing branch... (esc to cancel)",
1999
+ getSymbolTheme().spinnerFrames,
1933
2000
  );
1934
2001
  this.statusContainer.addChild(summaryLoader);
1935
2002
  this.ui.requestRender();
@@ -2092,7 +2159,11 @@ export class InteractiveMode {
2092
2159
  this.session.modelRegistry.refresh();
2093
2160
  this.chatContainer.addChild(new Spacer(1));
2094
2161
  this.chatContainer.addChild(
2095
- new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0),
2162
+ new Text(
2163
+ theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
2164
+ 1,
2165
+ 0,
2166
+ ),
2096
2167
  );
2097
2168
  this.chatContainer.addChild(
2098
2169
  new Text(theme.fg("dim", `Credentials saved to ${getAuthPath()}`), 1, 0),
@@ -2108,7 +2179,11 @@ export class InteractiveMode {
2108
2179
  this.session.modelRegistry.refresh();
2109
2180
  this.chatContainer.addChild(new Spacer(1));
2110
2181
  this.chatContainer.addChild(
2111
- new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),
2182
+ new Text(
2183
+ theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
2184
+ 1,
2185
+ 0,
2186
+ ),
2112
2187
  );
2113
2188
  this.chatContainer.addChild(
2114
2189
  new Text(theme.fg("dim", `Credentials removed from ${getAuthPath()}`), 1, 0),
@@ -2346,8 +2421,8 @@ export class InteractiveMode {
2346
2421
 
2347
2422
  this.chatContainer.addChild(new Spacer(1));
2348
2423
  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));
2424
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
2425
+ this.chatContainer.addChild(new Spacer(1));
2351
2426
  this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
2352
2427
  this.chatContainer.addChild(new DynamicBorder());
2353
2428
  this.ui.requestRender();
@@ -2420,7 +2495,9 @@ export class InteractiveMode {
2420
2495
  this.pendingTools.clear();
2421
2496
 
2422
2497
  this.chatContainer.addChild(new Spacer(1));
2423
- this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
2498
+ this.chatContainer.addChild(
2499
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
2500
+ );
2424
2501
  this.ui.requestRender();
2425
2502
  }
2426
2503
 
@@ -2451,7 +2528,11 @@ export class InteractiveMode {
2451
2528
 
2452
2529
  this.chatContainer.addChild(new Spacer(1));
2453
2530
  this.chatContainer.addChild(
2454
- new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1),
2531
+ new Text(
2532
+ `${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
2533
+ 1,
2534
+ 1,
2535
+ ),
2455
2536
  );
2456
2537
  this.ui.requestRender();
2457
2538
  }
@@ -2537,6 +2618,7 @@ export class InteractiveMode {
2537
2618
  (spinner) => theme.fg("accent", spinner),
2538
2619
  (text) => theme.fg("muted", text),
2539
2620
  label,
2621
+ getSymbolTheme().spinnerFrames,
2540
2622
  );
2541
2623
  this.statusContainer.addChild(compactingLoader);
2542
2624
  this.ui.requestRender();
@@ -2576,6 +2658,9 @@ export class InteractiveMode {
2576
2658
  if (this.unsubscribe) {
2577
2659
  this.unsubscribe();
2578
2660
  }
2661
+ if (this.cleanupUnsubscribe) {
2662
+ this.cleanupUnsubscribe();
2663
+ }
2579
2664
  if (this.isInitialized) {
2580
2665
  this.ui.stop();
2581
2666
  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,111 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/theme-schema.json",
3
+ "name": "dark-arctic",
4
+ "vars": {
5
+ "polarNight0": "#2e3440",
6
+ "polarNight1": "#3b4252",
7
+ "polarNight2": "#434c5e",
8
+ "polarNight3": "#4c566a",
9
+ "snowStorm0": "#d8dee9",
10
+ "snowStorm1": "#e5e9f0",
11
+ "snowStorm2": "#eceff4",
12
+ "frost0": "#8fbcbb",
13
+ "frost1": "#88c0d0",
14
+ "frost2": "#81a1c1",
15
+ "frost3": "#5e81ac",
16
+ "auroraRed": "#bf616a",
17
+ "auroraOrange": "#d08770",
18
+ "auroraYellow": "#ebcb8b",
19
+ "auroraGreen": "#a3be8c",
20
+ "auroraPurple": "#b48ead",
21
+ "iceBlue": "#88c0d0",
22
+ "deepPolar": "#242933",
23
+ "userMsgBg": "#1f2430",
24
+ "toolPendingBg": "#242933",
25
+ "toolSuccessBg": "#1f2835",
26
+ "toolErrorBg": "#2d2428",
27
+ "customMsgBg": "#2a2a3c"
28
+ },
29
+ "colors": {
30
+ "accent": "iceBlue",
31
+ "border": "frost3",
32
+ "borderAccent": "frost1",
33
+ "borderMuted": "polarNight3",
34
+ "success": "auroraGreen",
35
+ "error": "auroraRed",
36
+ "warning": "auroraYellow",
37
+ "muted": "polarNight3",
38
+ "dim": "polarNight2",
39
+ "text": "",
40
+ "thinkingText": "polarNight3",
41
+
42
+ "selectedBg": "polarNight1",
43
+ "userMessageBg": "userMsgBg",
44
+ "userMessageText": "",
45
+ "customMessageBg": "customMsgBg",
46
+ "customMessageText": "",
47
+ "customMessageLabel": "auroraPurple",
48
+ "toolPendingBg": "toolPendingBg",
49
+ "toolSuccessBg": "toolSuccessBg",
50
+ "toolErrorBg": "toolErrorBg",
51
+ "toolText": "",
52
+ "toolTitle": "",
53
+ "toolOutput": "polarNight3",
54
+
55
+ "mdHeading": "frost1",
56
+ "mdLink": "frost2",
57
+ "mdLinkUrl": "polarNight3",
58
+ "mdCode": "auroraPurple",
59
+ "mdCodeBlock": "snowStorm0",
60
+ "mdCodeBlockBorder": "polarNight2",
61
+ "mdQuote": "polarNight3",
62
+ "mdQuoteBorder": "polarNight2",
63
+ "mdHr": "polarNight2",
64
+ "mdListBullet": "iceBlue",
65
+
66
+ "toolDiffAdded": "auroraGreen",
67
+ "toolDiffRemoved": "auroraRed",
68
+ "toolDiffContext": "polarNight3",
69
+
70
+ "link": "frost2",
71
+
72
+ "syntaxComment": "#616e88",
73
+ "syntaxKeyword": "frost3",
74
+ "syntaxFunction": "frost1",
75
+ "syntaxVariable": "snowStorm0",
76
+ "syntaxString": "auroraGreen",
77
+ "syntaxNumber": "auroraPurple",
78
+ "syntaxType": "frost0",
79
+ "syntaxOperator": "frost2",
80
+ "syntaxPunctuation": "snowStorm1",
81
+
82
+ "thinkingOff": "polarNight2",
83
+ "thinkingMinimal": "polarNight3",
84
+ "thinkingLow": "frost3",
85
+ "thinkingMedium": "frost2",
86
+ "thinkingHigh": "auroraPurple",
87
+ "thinkingXhigh": "#c8a0d1",
88
+
89
+ "bashMode": "iceBlue",
90
+
91
+ "statusLineBg": "#1a1f2b",
92
+ "statusLineSep": 240,
93
+ "statusLineModel": "auroraPurple",
94
+ "statusLinePath": "frost0",
95
+ "statusLineGitClean": "auroraGreen",
96
+ "statusLineGitDirty": "auroraYellow",
97
+ "statusLineContext": "frost3",
98
+ "statusLineSpend": "frost1",
99
+ "statusLineStaged": 108,
100
+ "statusLineDirty": 180,
101
+ "statusLineUntracked": 109,
102
+ "statusLineOutput": 139,
103
+ "statusLineCost": 139,
104
+ "statusLineSubagents": "iceBlue"
105
+ },
106
+ "export": {
107
+ "pageBg": "#1a1f2b",
108
+ "cardBg": "#242933",
109
+ "infoBg": "#353a48"
110
+ }
111
+ }