@oh-my-pi/pi-coding-agent 15.5.15 → 15.6.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 (167) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/initial-message.d.ts +1 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  5. package/dist/types/commands/tiny-models.d.ts +22 -0
  6. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  7. package/dist/types/commit/analysis/summary.d.ts +1 -1
  8. package/dist/types/commit/changelog/generate.d.ts +1 -1
  9. package/dist/types/commit/changelog/index.d.ts +2 -2
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  12. package/dist/types/config/model-id-affixes.d.ts +10 -0
  13. package/dist/types/config/settings-schema.d.ts +232 -7
  14. package/dist/types/discovery/helpers.d.ts +1 -1
  15. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  16. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  17. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  18. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  19. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  20. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  21. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  22. package/dist/types/internal-urls/router.d.ts +8 -1
  23. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  24. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  25. package/dist/types/internal-urls/types.d.ts +26 -0
  26. package/dist/types/memory-backend/index.d.ts +1 -0
  27. package/dist/types/memory-backend/resolve.d.ts +2 -1
  28. package/dist/types/memory-backend/types.d.ts +7 -1
  29. package/dist/types/mnemosyne/backend.d.ts +4 -0
  30. package/dist/types/mnemosyne/config.d.ts +29 -0
  31. package/dist/types/mnemosyne/index.d.ts +3 -0
  32. package/dist/types/mnemosyne/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  34. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  35. package/dist/types/modes/components/index.d.ts +1 -0
  36. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  37. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  38. package/dist/types/modes/components/welcome.d.ts +1 -0
  39. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  40. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  43. package/dist/types/modes/orchestrate.d.ts +10 -0
  44. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  45. package/dist/types/modes/ultrathink.d.ts +3 -3
  46. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  47. package/dist/types/sdk.d.ts +3 -0
  48. package/dist/types/session/agent-session.d.ts +33 -0
  49. package/dist/types/system-prompt.d.ts +2 -0
  50. package/dist/types/task/executor.d.ts +2 -0
  51. package/dist/types/task/render.d.ts +5 -1
  52. package/dist/types/tiny/models.d.ts +185 -0
  53. package/dist/types/tiny/text.d.ts +4 -0
  54. package/dist/types/tiny/title-client.d.ts +24 -0
  55. package/dist/types/tiny/title-protocol.d.ts +74 -0
  56. package/dist/types/tiny/worker.d.ts +2 -0
  57. package/dist/types/tools/bash.d.ts +3 -1
  58. package/dist/types/tools/index.d.ts +7 -3
  59. package/dist/types/tools/memory-edit.d.ts +40 -0
  60. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  61. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  62. package/dist/types/tools/memory-render.d.ts +60 -0
  63. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  64. package/dist/types/tools/todo-write.d.ts +8 -0
  65. package/dist/types/tools/tool-result.d.ts +2 -0
  66. package/dist/types/utils/title-generator.d.ts +3 -0
  67. package/package.json +18 -14
  68. package/scripts/build-binary.ts +1 -0
  69. package/src/cli/tiny-models-cli.ts +127 -0
  70. package/src/cli-commands.ts +1 -0
  71. package/src/cli.ts +8 -8
  72. package/src/commands/tiny-models.ts +36 -0
  73. package/src/config/model-equivalence.ts +43 -2
  74. package/src/config/model-id-affixes.ts +64 -0
  75. package/src/config/model-registry.ts +84 -10
  76. package/src/config/settings-schema.ts +205 -4
  77. package/src/edit/hashline/diff.ts +5 -7
  78. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  79. package/src/eval/js/shared/local-module-loader.ts +13 -1
  80. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  81. package/src/internal-urls/agent-protocol.ts +18 -1
  82. package/src/internal-urls/artifact-protocol.ts +19 -1
  83. package/src/internal-urls/docs-index.generated.ts +3 -1
  84. package/src/internal-urls/local-protocol.ts +14 -1
  85. package/src/internal-urls/memory-protocol.ts +6 -1
  86. package/src/internal-urls/omp-protocol.ts +5 -1
  87. package/src/internal-urls/router.ts +20 -1
  88. package/src/internal-urls/rule-protocol.ts +8 -1
  89. package/src/internal-urls/skill-protocol.ts +8 -1
  90. package/src/internal-urls/types.ts +27 -0
  91. package/src/lsp/render.ts +1 -1
  92. package/src/mcp/oauth-flow.ts +2 -2
  93. package/src/memory-backend/index.ts +1 -0
  94. package/src/memory-backend/resolve.ts +4 -1
  95. package/src/memory-backend/types.ts +8 -1
  96. package/src/mnemosyne/backend.ts +374 -0
  97. package/src/mnemosyne/config.ts +160 -0
  98. package/src/mnemosyne/index.ts +3 -0
  99. package/src/mnemosyne/state.ts +548 -0
  100. package/src/modes/acp/acp-agent.ts +11 -6
  101. package/src/modes/components/agent-dashboard.ts +4 -4
  102. package/src/modes/components/custom-editor.ts +3 -2
  103. package/src/modes/components/diff.ts +2 -2
  104. package/src/modes/components/extensions/extension-list.ts +3 -2
  105. package/src/modes/components/footer.ts +5 -6
  106. package/src/modes/components/history-search.ts +3 -3
  107. package/src/modes/components/hook-selector.ts +94 -8
  108. package/src/modes/components/index.ts +1 -0
  109. package/src/modes/components/mcp-add-wizard.ts +3 -3
  110. package/src/modes/components/model-selector.ts +5 -4
  111. package/src/modes/components/oauth-selector.ts +3 -3
  112. package/src/modes/components/session-observer-overlay.ts +19 -13
  113. package/src/modes/components/session-selector.ts +3 -3
  114. package/src/modes/components/settings-defs.ts +7 -0
  115. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  116. package/src/modes/components/status-line/segments.ts +2 -2
  117. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  118. package/src/modes/components/tips.txt +12 -0
  119. package/src/modes/components/tool-execution.ts +67 -3
  120. package/src/modes/components/tree-selector.ts +3 -3
  121. package/src/modes/components/user-message-selector.ts +3 -3
  122. package/src/modes/components/welcome.ts +55 -1
  123. package/src/modes/controllers/command-controller.ts +16 -1
  124. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  125. package/src/modes/controllers/input-controller.ts +57 -0
  126. package/src/modes/gradient-highlight.ts +70 -0
  127. package/src/modes/interactive-mode.ts +58 -109
  128. package/src/modes/internal-url-autocomplete.ts +143 -0
  129. package/src/modes/orchestrate.ts +36 -0
  130. package/src/modes/prompt-action-autocomplete.ts +12 -0
  131. package/src/modes/ultrathink.ts +9 -53
  132. package/src/modes/utils/keybinding-matchers.ts +11 -0
  133. package/src/prompts/system/memory-consolidation-system.md +8 -0
  134. package/src/prompts/system/memory-extraction-system.md +26 -0
  135. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
  136. package/src/prompts/system/system-prompt.md +2 -0
  137. package/src/prompts/system/tiny-title-system.md +8 -0
  138. package/src/prompts/tools/memory-edit.md +8 -0
  139. package/src/prompts/tools/task.md +4 -7
  140. package/src/sdk.ts +8 -6
  141. package/src/session/agent-session.ts +128 -44
  142. package/src/slash-commands/builtin-registry.ts +10 -1
  143. package/src/system-prompt.ts +4 -0
  144. package/src/task/commands.ts +1 -5
  145. package/src/task/executor.ts +8 -0
  146. package/src/task/index.ts +2 -0
  147. package/src/task/render.ts +69 -26
  148. package/src/tiny/models.ts +217 -0
  149. package/src/tiny/text.ts +19 -0
  150. package/src/tiny/title-client.ts +340 -0
  151. package/src/tiny/title-protocol.ts +51 -0
  152. package/src/tiny/worker.ts +523 -0
  153. package/src/tools/bash.ts +58 -16
  154. package/src/tools/browser/tab-worker.ts +1 -1
  155. package/src/tools/index.ts +17 -11
  156. package/src/tools/memory-edit.ts +59 -0
  157. package/src/tools/memory-recall.ts +100 -0
  158. package/src/tools/memory-reflect.ts +88 -0
  159. package/src/tools/memory-render.ts +185 -0
  160. package/src/tools/memory-retain.ts +91 -0
  161. package/src/tools/renderers.ts +4 -0
  162. package/src/tools/todo-write.ts +128 -29
  163. package/src/tools/tool-result.ts +8 -0
  164. package/src/utils/title-generator.ts +115 -13
  165. package/src/tools/hindsight-recall.ts +0 -69
  166. package/src/tools/hindsight-reflect.ts +0 -58
  167. package/src/tools/hindsight-retain.ts +0 -57
@@ -7,7 +7,7 @@ import type { AgentSession } from "../../session/agent-session";
7
7
  import { shortenPath } from "../../tools/render-utils";
8
8
  import * as git from "../../utils/git";
9
9
  import { sanitizeStatusText } from "../shared";
10
- import { getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
10
+ import { formatContextUsage, getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
11
11
 
12
12
  /**
13
13
  * Footer component that shows pwd, token stats, and context usage
@@ -136,7 +136,6 @@ export class FooterComponent implements Component {
136
136
  const contextUsage = this.session.getContextUsage();
137
137
  const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
138
138
  const contextPercentValue = contextUsage?.percent ?? 0;
139
- const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
140
139
 
141
140
  // Replace home directory with ~
142
141
  let pwd = shortenPath(getProjectDir());
@@ -180,10 +179,10 @@ export class FooterComponent implements Component {
180
179
  // Colorize context percentage based on usage
181
180
  let contextPercentStr: string;
182
181
  const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
183
- const contextPercentDisplay =
184
- contextPercent === "?"
185
- ? `?/${formatNumber(contextWindow)}${autoIndicator}`
186
- : `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
182
+ const contextPercentDisplay = `${formatContextUsage(
183
+ contextUsage?.percent === null ? null : contextPercentValue,
184
+ contextWindow,
185
+ )}${autoIndicator}`;
187
186
  if (contextUsage?.percent !== null && contextUsage?.percent !== undefined) {
188
187
  const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
189
188
  contextPercentStr =
@@ -11,7 +11,7 @@ import {
11
11
  visibleWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import { theme } from "../../modes/theme/theme";
14
- import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
14
+ import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
15
15
  import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
16
16
  import { DynamicBorder } from "./dynamic-border";
17
17
 
@@ -116,14 +116,14 @@ export class HistorySearchComponent extends Container {
116
116
  }
117
117
 
118
118
  handleInput(keyData: string): void {
119
- if (matchesKey(keyData, "up")) {
119
+ if (matchesSelectUp(keyData)) {
120
120
  if (this.#results.length === 0) return;
121
121
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
122
122
  this.#resultsList.setSelectedIndex(this.#selectedIndex);
123
123
  return;
124
124
  }
125
125
 
126
- if (matchesKey(keyData, "down")) {
126
+ if (matchesSelectDown(keyData)) {
127
127
  if (this.#results.length === 0) return;
128
128
  this.#selectedIndex = Math.min(this.#results.length - 1, this.#selectedIndex + 1);
129
129
  this.#resultsList.setSelectedIndex(this.#selectedIndex);
@@ -15,11 +15,43 @@ import {
15
15
  truncateToWidth,
16
16
  visibleWidth,
17
17
  } from "@oh-my-pi/pi-tui";
18
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
19
- import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
18
+ import { getMarkdownTheme, type ThemeColor, theme } from "../../modes/theme/theme";
19
+ import {
20
+ matchesAppExternalEditor,
21
+ matchesSelectCancel,
22
+ matchesSelectDown,
23
+ matchesSelectUp,
24
+ } from "../../modes/utils/keybinding-matchers";
20
25
  import { CountdownTimer } from "./countdown-timer";
21
26
  import { DynamicBorder } from "./dynamic-border";
22
27
 
28
+ /** One segment of a {@link HookSelectorSlider} — a label, its accent color, and
29
+ * an optional detail line (e.g. the resolved model name) shown beneath the
30
+ * track while the segment is active. */
31
+ export interface HookSelectorSliderSegment {
32
+ label: string;
33
+ /** Theme color for the segment label; defaults to `accent`. */
34
+ color?: ThemeColor;
35
+ /** Secondary line rendered under the track when this segment is selected. */
36
+ detail?: string;
37
+ }
38
+
39
+ /**
40
+ * A horizontal left/right selector rendered above the option list. Unlike the
41
+ * up/down option cursor, the slider is moved with the left/right arrows from
42
+ * any list position, letting the caller capture an orthogonal choice (e.g. the
43
+ * model tier to continue execution with) alongside the selected option.
44
+ */
45
+ export interface HookSelectorSlider {
46
+ /** Dim caption rendered before the slider track (e.g. "continue with"). */
47
+ caption?: string;
48
+ segments: HookSelectorSliderSegment[];
49
+ /** Initially highlighted segment index. */
50
+ index: number;
51
+ /** Invoked with the new index whenever the slider moves. */
52
+ onChange?: (index: number) => void;
53
+ }
54
+
23
55
  export interface HookSelectorOptions {
24
56
  tui?: TUI;
25
57
  timeout?: number;
@@ -31,6 +63,7 @@ export interface HookSelectorOptions {
31
63
  onRight?: () => void;
32
64
  onExternalEditor?: () => void;
33
65
  helpText?: string;
66
+ slider?: HookSelectorSlider;
34
67
  }
35
68
 
36
69
  class OutlinedList extends Container {
@@ -69,6 +102,9 @@ export class HookSelectorComponent extends Container {
69
102
  #onLeftCallback: (() => void) | undefined;
70
103
  #onRightCallback: (() => void) | undefined;
71
104
  #onExternalEditorCallback: (() => void) | undefined;
105
+ #slider: HookSelectorSlider | undefined;
106
+ #sliderIndex: number = 0;
107
+ #sliderComponent: Text | undefined;
72
108
  constructor(
73
109
  title: string,
74
110
  options: string[],
@@ -87,6 +123,10 @@ export class HookSelectorComponent extends Container {
87
123
  this.#onLeftCallback = opts?.onLeft;
88
124
  this.#onRightCallback = opts?.onRight;
89
125
  this.#onExternalEditorCallback = opts?.onExternalEditor;
126
+ if (opts?.slider && opts.slider.segments.length > 0) {
127
+ this.#slider = opts.slider;
128
+ this.#sliderIndex = Math.max(0, Math.min(opts.slider.index, opts.slider.segments.length - 1));
129
+ }
90
130
 
91
131
  this.addChild(new DynamicBorder());
92
132
  this.addChild(new Spacer(1));
@@ -95,6 +135,12 @@ export class HookSelectorComponent extends Container {
95
135
  this.addChild(this.#titleComponent);
96
136
  this.addChild(new Spacer(1));
97
137
 
138
+ if (this.#slider) {
139
+ this.#sliderComponent = new Text(this.#renderSliderLine(), 1, 0);
140
+ this.addChild(this.#sliderComponent);
141
+ this.addChild(new Spacer(1));
142
+ }
143
+
98
144
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
99
145
  this.#countdown = new CountdownTimer(
100
146
  opts.timeout,
@@ -160,23 +206,63 @@ export class HookSelectorComponent extends Container {
160
206
  }
161
207
  }
162
208
 
209
+ /** Render the slider block: the track (dim caption, edge arrows that brighten
210
+ * while there is room to move, one styled segment per option — active = bold
211
+ * in its color, the rest dim, joined by `›`) plus, when the active segment
212
+ * carries a `detail`, a muted second line beneath it (e.g. the resolved model
213
+ * name). Returns one or two `\n`-joined lines. */
214
+ #renderSliderLine(): string {
215
+ const slider = this.#slider;
216
+ if (!slider) return "";
217
+ const segments = slider.segments;
218
+ const sep = theme.fg("dim", " › ");
219
+ const track = segments
220
+ .map((segment, i) =>
221
+ i === this.#sliderIndex
222
+ ? theme.bold(theme.fg(segment.color ?? "accent", segment.label))
223
+ : theme.fg("dim", segment.label),
224
+ )
225
+ .join(sep);
226
+ const leftArrow = theme.fg(this.#sliderIndex > 0 ? "accent" : "dim", "◂");
227
+ const rightArrow = theme.fg(this.#sliderIndex < segments.length - 1 ? "accent" : "dim", "▸");
228
+ const caption = slider.caption ? `${theme.fg("dim", slider.caption)} ` : "";
229
+ const trackLine = `${caption}${leftArrow} ${theme.fg("dim", "[")} ${track} ${theme.fg("dim", "]")} ${rightArrow}`;
230
+ const detail = segments[this.#sliderIndex]?.detail;
231
+ if (!detail) return trackLine;
232
+ return `${trackLine}\n ${theme.fg("dim", "↳")} ${theme.fg("muted", detail)}`;
233
+ }
234
+
235
+ /** Move the slider by `delta`, clamped to the segment range, refresh the
236
+ * rendered track, and notify the caller only when the index actually moves. */
237
+ #moveSlider(delta: number): void {
238
+ const slider = this.#slider;
239
+ if (!slider) return;
240
+ const next = Math.max(0, Math.min(slider.segments.length - 1, this.#sliderIndex + delta));
241
+ if (next === this.#sliderIndex) return;
242
+ this.#sliderIndex = next;
243
+ this.#sliderComponent?.setText(this.#renderSliderLine());
244
+ slider.onChange?.(next);
245
+ }
246
+
163
247
  handleInput(keyData: string): void {
164
248
  // Reset countdown on any interaction
165
249
  this.#countdown?.reset();
166
250
 
167
- if (matchesKey(keyData, "up") || keyData === "k") {
251
+ if (matchesSelectUp(keyData) || keyData === "k") {
168
252
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
169
253
  this.#updateList();
170
- } else if (matchesKey(keyData, "down") || keyData === "j") {
254
+ } else if (matchesSelectDown(keyData) || keyData === "j") {
171
255
  this.#selectedIndex = Math.min(this.#options.length - 1, this.#selectedIndex + 1);
172
256
  this.#updateList();
173
257
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
174
258
  const selected = this.#options[this.#selectedIndex];
175
259
  if (selected) this.#onSelectCallback(selected);
176
- } else if (matchesKey(keyData, "left")) {
177
- this.#onLeftCallback?.();
178
- } else if (matchesKey(keyData, "right")) {
179
- this.#onRightCallback?.();
260
+ } else if (matchesKey(keyData, "left") || (this.#slider && keyData === "h")) {
261
+ if (this.#slider) this.#moveSlider(-1);
262
+ else this.#onLeftCallback?.();
263
+ } else if (matchesKey(keyData, "right") || (this.#slider && keyData === "l")) {
264
+ if (this.#slider) this.#moveSlider(1);
265
+ else this.#onRightCallback?.();
180
266
  } else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
181
267
  this.#onExternalEditorCallback();
182
268
  } else if (matchesSelectCancel(keyData)) {
@@ -26,6 +26,7 @@ export * from "./show-images-selector";
26
26
  export * from "./status-line";
27
27
  export * from "./theme-selector";
28
28
  export * from "./thinking-selector";
29
+ export * from "./tiny-title-download-progress";
29
30
  export * from "./todo-reminder";
30
31
  export * from "./tool-execution";
31
32
  export * from "./tree-selector";
@@ -19,7 +19,7 @@ import { analyzeAuthError, discoverOAuthEndpoints } from "../../mcp/oauth-discov
19
19
  import type { MCPHttpServerConfig, MCPServerConfig, MCPSseServerConfig, MCPStdioServerConfig } from "../../mcp/types";
20
20
  import { shortenPath } from "../../tools/render-utils";
21
21
  import { theme } from "../theme/theme";
22
- import { matchesAppInterrupt } from "../utils/keybinding-matchers";
22
+ import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
23
23
  import { DynamicBorder } from "./dynamic-border";
24
24
 
25
25
  type TransportType = "stdio" | "http" | "sse";
@@ -501,11 +501,11 @@ export class MCPAddWizard extends Container {
501
501
  }
502
502
 
503
503
  // Handle up/down arrows for selectors
504
- if (matchesKey(keyData, "up")) {
504
+ if (matchesSelectUp(keyData)) {
505
505
  this.#moveSelection(-1);
506
506
  return;
507
507
  }
508
- if (matchesKey(keyData, "down")) {
508
+ if (matchesSelectDown(keyData)) {
509
509
  this.#moveSelection(1);
510
510
  return;
511
511
  }
@@ -18,6 +18,7 @@ import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../..
18
18
  import { resolveModelRoleValue } from "../../config/model-resolver";
19
19
  import type { Settings } from "../../config/settings";
20
20
  import { type ThemeColor, theme } from "../../modes/theme/theme";
21
+ import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
21
22
  import { getThinkingLevelMetadata } from "../../thinking";
22
23
  import { getTabBarTheme } from "../shared";
23
24
  import { DynamicBorder } from "./dynamic-border";
@@ -971,7 +972,7 @@ export class ModelSelectorComponent extends Container {
971
972
  }
972
973
 
973
974
  // Up arrow - navigate list (wrap to bottom when at top)
974
- if (matchesKey(keyData, "up")) {
975
+ if (matchesSelectUp(keyData)) {
975
976
  const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
976
977
  if (itemCount === 0) return;
977
978
  this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
@@ -980,7 +981,7 @@ export class ModelSelectorComponent extends Container {
980
981
  }
981
982
 
982
983
  // Down arrow - navigate list (wrap to top when at bottom)
983
- if (matchesKey(keyData, "down")) {
984
+ if (matchesSelectDown(keyData)) {
984
985
  const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
985
986
  if (itemCount === 0) return;
986
987
  this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
@@ -1022,13 +1023,13 @@ export class ModelSelectorComponent extends Container {
1022
1023
  : this.#menuRoleActions.length;
1023
1024
  if (optionCount === 0) return;
1024
1025
 
1025
- if (matchesKey(keyData, "up")) {
1026
+ if (matchesSelectUp(keyData)) {
1026
1027
  this.#menuSelectedIndex = (this.#menuSelectedIndex - 1 + optionCount) % optionCount;
1027
1028
  this.#updateMenu();
1028
1029
  return;
1029
1030
  }
1030
1031
 
1031
- if (matchesKey(keyData, "down")) {
1032
+ if (matchesSelectDown(keyData)) {
1032
1033
  this.#menuSelectedIndex = (this.#menuSelectedIndex + 1) % optionCount;
1033
1034
  this.#updateMenu();
1034
1035
  return;
@@ -2,7 +2,7 @@ import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
2
2
  import type { OAuthProviderInfo } from "@oh-my-pi/pi-ai/utils/oauth/types";
3
3
  import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
4
4
  import { theme } from "../../modes/theme/theme";
5
- import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
5
+ import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
6
6
  import type { AuthStorage } from "../../session/auth-storage";
7
7
  import { DynamicBorder } from "./dynamic-border";
8
8
 
@@ -193,7 +193,7 @@ export class OAuthSelectorComponent extends Container {
193
193
  }
194
194
  handleInput(keyData: string): void {
195
195
  // Up arrow
196
- if (matchesKey(keyData, "up")) {
196
+ if (matchesSelectUp(keyData)) {
197
197
  if (this.#allProviders.length > 0) {
198
198
  this.#selectedIndex = this.#selectedIndex === 0 ? this.#allProviders.length - 1 : this.#selectedIndex - 1;
199
199
  }
@@ -201,7 +201,7 @@ export class OAuthSelectorComponent extends Container {
201
201
  this.#updateList();
202
202
  }
203
203
  // Down arrow
204
- else if (matchesKey(keyData, "down")) {
204
+ else if (matchesSelectDown(keyData)) {
205
205
  if (this.#allProviders.length > 0) {
206
206
  this.#selectedIndex = this.#selectedIndex === this.#allProviders.length - 1 ? 0 : this.#selectedIndex + 1;
207
207
  }
@@ -25,7 +25,9 @@ import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "
25
25
  import { toPathList } from "../../tools/search";
26
26
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
27
27
  import { getMarkdownTheme, theme } from "../theme/theme";
28
+ import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
28
29
  import { DynamicBorder } from "./dynamic-border";
30
+ import { formatContextUsage } from "./status-line/context-thresholds";
29
31
 
30
32
  /** Max thinking characters in collapsed state */
31
33
  const MAX_THINKING_CHARS_COLLAPSED = 200;
@@ -266,23 +268,27 @@ export class SessionObserverOverlayComponent extends Container {
266
268
  const progress = session?.progress;
267
269
  if (!progress) return "";
268
270
  const stats: string[] = [];
269
- if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
270
- // Current per-turn context — what the user reads as "how full is the context".
271
- // Falls back to cumulative billing volume (Σ-prefixed) when context size is unknown.
271
+ // Current per-turn context — match the status line's `<pct>%/<window>` gauge (e.g. `5.1%/1M`).
272
272
  if (progress.contextTokens && progress.contextTokens > 0) {
273
273
  const ctx =
274
274
  progress.contextWindow && progress.contextWindow > 0
275
- ? `${formatNumber(progress.contextTokens)}/${formatNumber(progress.contextWindow)} ctx`
276
- : `${formatNumber(progress.contextTokens)} ctx`;
275
+ ? formatContextUsage((progress.contextTokens / progress.contextWindow) * 100, progress.contextWindow)
276
+ : `${formatNumber(progress.contextTokens)}`;
277
277
  stats.push(ctx);
278
- if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
279
- } else if (progress.tokens > 0) {
280
- stats.push(`Σ${formatNumber(progress.tokens)}`);
281
278
  }
282
- if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
279
+ if (progress.durationMs > 0) {
280
+ stats.push(formatDuration(progress.durationMs));
281
+ }
283
282
  const parts: string[] = [];
284
- if (stats.length > 0) parts.push(theme.fg("dim", stats.join(theme.sep.dot)));
285
- if (progress.cost > 0) parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
283
+ if (stats.length > 0 || progress.toolCount > 0) {
284
+ const toolCountStat =
285
+ progress.toolCount > 0 ? `${formatNumber(progress.toolCount)} ${theme.icon.extensionTool}` : undefined;
286
+ const statSegments = [toolCountStat, ...stats].filter((segment): segment is string => Boolean(segment));
287
+ parts.push(theme.fg("dim", statSegments.join(theme.sep.dot)));
288
+ }
289
+ if (progress.cost > 0) {
290
+ parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
291
+ }
286
292
  return parts.join(theme.sep.dot);
287
293
  }
288
294
 
@@ -660,7 +666,7 @@ export class SessionObserverOverlayComponent extends Container {
660
666
  }
661
667
 
662
668
  // j / down — move selection down
663
- if (keyData === "j" || matchesKey(keyData, "down")) {
669
+ if (keyData === "j" || matchesSelectDown(keyData)) {
664
670
  if (entryCount > 0) {
665
671
  this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 1, entryCount - 1);
666
672
  }
@@ -669,7 +675,7 @@ export class SessionObserverOverlayComponent extends Container {
669
675
  }
670
676
 
671
677
  // k / up — move selection up
672
- if (keyData === "k" || matchesKey(keyData, "up")) {
678
+ if (keyData === "k" || matchesSelectUp(keyData)) {
673
679
  if (entryCount > 0) {
674
680
  this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 1, 0);
675
681
  }
@@ -13,7 +13,7 @@ import {
13
13
  } from "@oh-my-pi/pi-tui";
14
14
  import { formatBytes } from "@oh-my-pi/pi-utils";
15
15
  import { theme } from "../../modes/theme/theme";
16
- import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
16
+ import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
17
17
  import type { SessionInfo } from "../../session/session-manager";
18
18
  import { DynamicBorder } from "./dynamic-border";
19
19
  import { HookSelectorComponent } from "./hook-selector";
@@ -192,12 +192,12 @@ class SessionList implements Component {
192
192
  }
193
193
 
194
194
  // Up arrow
195
- if (matchesKey(keyData, "up")) {
195
+ if (matchesSelectUp(keyData)) {
196
196
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
197
197
  return;
198
198
  }
199
199
  // Down arrow
200
- if (matchesKey(keyData, "down")) {
200
+ if (matchesSelectDown(keyData)) {
201
201
  this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + 1);
202
202
  return;
203
203
  }
@@ -79,6 +79,13 @@ const CONDITIONS: Record<string, () => boolean> = {
79
79
  return false;
80
80
  }
81
81
  },
82
+ mnemosyneActive: () => {
83
+ try {
84
+ return Settings.instance.get("memory.backend") === "mnemosyne";
85
+ } catch {
86
+ return false;
87
+ }
88
+ },
82
89
  };
83
90
 
84
91
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1,3 +1,4 @@
1
+ import { formatNumber } from "@oh-my-pi/pi-utils";
1
2
  import type { ThemeColor } from "../../../modes/theme/theme";
2
3
 
3
4
  export type ContextUsageLevel = "normal" | "warning" | "purple" | "error";
@@ -54,6 +55,16 @@ export function getContextUsageLevel(contextPercent: number, contextWindow: numb
54
55
  return "normal";
55
56
  }
56
57
 
58
+ /**
59
+ * Format context usage as `<percent>%/<window>` (e.g. `5.1%/1M`), matching the
60
+ * status line's context gauge so subagent and footer renderers stay in sync.
61
+ * A `null`/`undefined` percent (unknown, e.g. right after compaction) renders as `?`.
62
+ */
63
+ export function formatContextUsage(contextPercent: number | null | undefined, contextWindow: number): string {
64
+ const pct = contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`;
65
+ return `${pct}/${formatNumber(contextWindow)}`;
66
+ }
67
+
57
68
  export function getContextUsageThemeColor(level: ContextUsageLevel): ThemeColor {
58
69
  switch (level) {
59
70
  case "error":
@@ -7,7 +7,7 @@ import { type ThemeColor, theme } from "../../../modes/theme/theme";
7
7
  import { shortenPath } from "../../../tools/render-utils";
8
8
  import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
9
9
  import { sanitizeStatusText } from "../../shared";
10
- import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
10
+ import { formatContextUsage, getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
11
11
  import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
12
12
 
13
13
  export type { SegmentContext } from "./types";
@@ -350,7 +350,7 @@ const contextPctSegment: StatusLineSegment = {
350
350
  const window = ctx.contextWindow;
351
351
 
352
352
  const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
353
- const text = `${pct.toFixed(1)}%/${formatNumber(window)}${autoIcon}`;
353
+ const text = `${formatContextUsage(pct, window)}${autoIcon}`;
354
354
 
355
355
  const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
356
356
  const content = withIcon(theme.icon.context, theme.fg(color, text));
@@ -0,0 +1,90 @@
1
+ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import { formatBytes } from "@oh-my-pi/pi-utils";
3
+ import { getTinyTitleModelSpec, type TinyTitleLocalModelKey } from "../../tiny/models";
4
+ import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
5
+ import { theme } from "../theme/theme";
6
+
7
+ const DEFAULT_BAR_WIDTH = 24;
8
+
9
+ function padLine(line: string, width: number): string {
10
+ const visible = visibleWidth(line);
11
+ return visible >= width ? truncateToWidth(line, width) : `${line}${" ".repeat(width - visible)}`;
12
+ }
13
+
14
+ function progressBar(progress: number | undefined, width: number): string {
15
+ const barWidth = Math.max(8, Math.min(DEFAULT_BAR_WIDTH, width));
16
+ if (progress === undefined) return theme.fg("muted", "░".repeat(barWidth));
17
+ const ratio = Math.max(0, Math.min(1, progress / 100));
18
+ const filled = Math.round(ratio * barWidth);
19
+ return `${theme.fg("accent", "█".repeat(filled))}${theme.fg("muted", "░".repeat(barWidth - filled))}`;
20
+ }
21
+
22
+ function currentFile(event: TinyTitleProgressEvent | undefined): string | undefined {
23
+ if (!event) return undefined;
24
+ if (event.file) return event.file.split("/").at(-1) ?? event.file;
25
+ if (event.files) {
26
+ let largestFile: string | undefined;
27
+ let largestLoaded = -1;
28
+ for (const file in event.files) {
29
+ const state = event.files[file];
30
+ if (state.loaded <= largestLoaded || state.loaded >= state.total) continue;
31
+ largestFile = file;
32
+ largestLoaded = state.loaded;
33
+ }
34
+ return largestFile?.split("/").at(-1) ?? largestFile;
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ function statusLabel(event: TinyTitleProgressEvent | undefined): string {
40
+ if (!event) return "Preparing";
41
+ if (event.status === "error") return "Failed";
42
+ if (event.status === "ready") return "Ready";
43
+ if (event.status === "done") return "Downloaded";
44
+ if (event.status === "download") return "Downloading";
45
+ if (event.status === "progress" || event.status === "progress_total") return "Downloading";
46
+ return "Preparing";
47
+ }
48
+
49
+ function byteLabel(event: TinyTitleProgressEvent | undefined): string | undefined {
50
+ if (!event?.loaded || !event.total) return undefined;
51
+ return `${formatBytes(event.loaded)} / ${formatBytes(event.total)}`;
52
+ }
53
+
54
+ export class TinyTitleDownloadProgressComponent implements Component {
55
+ #modelKey: TinyTitleLocalModelKey;
56
+ #event: TinyTitleProgressEvent | undefined;
57
+
58
+ constructor(modelKey: TinyTitleLocalModelKey) {
59
+ this.#modelKey = modelKey;
60
+ }
61
+
62
+ update(event: TinyTitleProgressEvent): void {
63
+ this.#event = event;
64
+ }
65
+
66
+ isComplete(): boolean {
67
+ return this.#event?.status === "ready" || this.#event?.status === "error";
68
+ }
69
+
70
+ invalidate(): void {
71
+ // No cached state.
72
+ }
73
+
74
+ render(width: number): string[] {
75
+ width = Math.max(1, width);
76
+ const spec = getTinyTitleModelSpec(this.#modelKey);
77
+ const border = theme.fg("border", theme.boxSharp.horizontal.repeat(width));
78
+ const status = statusLabel(this.#event);
79
+ const file = currentFile(this.#event);
80
+ const pct =
81
+ this.#event?.progress === undefined ? "" : `${Math.floor(this.#event.progress).toString().padStart(3, " ")}%`;
82
+ const bytes = byteLabel(this.#event);
83
+ const title = `${theme.fg("accent", "Tiny model")} ${theme.fg("muted", status)} ${spec.label}`;
84
+ const details = [progressBar(this.#event?.progress, Math.max(8, width - 36)), pct, bytes, file]
85
+ .filter((part): part is string => Boolean(part))
86
+ .join(" ");
87
+
88
+ return [border, padLine(` ${title}`, width), padLine(` ${details}`, width), border];
89
+ }
90
+ }
@@ -0,0 +1,12 @@
1
+ Tired of typing "keep going"? Just send a '.'
2
+ You can /btw to ask a side question
3
+ Ctrl+D can be used to exit, but with your draft saved!
4
+ Find out which model you emotionally abuse the most with `omp stats`
5
+ Try task isolation to create CoW worktrees
6
+ Your LLM can call an LLM using `llm(x...)`. Have a big batch of tasks? Ask clanker to use it!
7
+ Next time you see spaghet try: "omp, create a TTSR rule that will prevent this pattern, use omp://"
8
+ Did you know? Each kitty/tmux split keeps its own session — `omp -c` resumes the right one
9
+ Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
10
+ Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
11
+ Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
12
+ Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit