@oh-my-pi/pi-coding-agent 15.5.15 → 15.7.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 (274) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/classify-install-target.d.ts +0 -10
  5. package/dist/types/cli/completion-gen.d.ts +80 -0
  6. package/dist/types/cli/initial-message.d.ts +1 -1
  7. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  8. package/dist/types/commands/complete.d.ts +6 -0
  9. package/dist/types/commands/completions.d.ts +13 -0
  10. package/dist/types/commands/setup.d.ts +10 -1
  11. package/dist/types/commands/tiny-models.d.ts +22 -0
  12. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  13. package/dist/types/commit/analysis/summary.d.ts +1 -1
  14. package/dist/types/commit/changelog/generate.d.ts +1 -1
  15. package/dist/types/commit/changelog/index.d.ts +2 -2
  16. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  17. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  18. package/dist/types/config/model-id-affixes.d.ts +10 -0
  19. package/dist/types/config/settings-schema.d.ts +402 -17
  20. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  21. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  22. package/dist/types/discovery/helpers.d.ts +1 -1
  23. package/dist/types/discovery/index.d.ts +1 -0
  24. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  25. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  26. package/dist/types/edit/hashline/index.d.ts +1 -0
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  28. package/dist/types/eval/py/kernel.d.ts +3 -0
  29. package/dist/types/eval/py/runtime.d.ts +11 -1
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  32. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  33. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  34. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  35. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  36. package/dist/types/internal-urls/router.d.ts +8 -1
  37. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  38. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  39. package/dist/types/internal-urls/types.d.ts +26 -0
  40. package/dist/types/main.d.ts +1 -0
  41. package/dist/types/memory-backend/index.d.ts +1 -0
  42. package/dist/types/memory-backend/resolve.d.ts +2 -1
  43. package/dist/types/memory-backend/types.d.ts +7 -1
  44. package/dist/types/mnemosyne/backend.d.ts +4 -0
  45. package/dist/types/mnemosyne/config.d.ts +29 -0
  46. package/dist/types/mnemosyne/index.d.ts +3 -0
  47. package/dist/types/mnemosyne/state.d.ts +72 -0
  48. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  49. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  50. package/dist/types/modes/components/index.d.ts +2 -0
  51. package/dist/types/modes/components/segment-track.d.ts +22 -0
  52. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  53. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  54. package/dist/types/modes/components/welcome.d.ts +22 -0
  55. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  56. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  57. package/dist/types/modes/interactive-mode.d.ts +7 -4
  58. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  59. package/dist/types/modes/orchestrate.d.ts +10 -0
  60. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  61. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  62. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  63. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  64. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  65. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  66. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  70. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  71. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  72. package/dist/types/modes/theme/theme.d.ts +11 -0
  73. package/dist/types/modes/types.d.ts +5 -1
  74. package/dist/types/modes/ultrathink.d.ts +3 -3
  75. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  76. package/dist/types/sdk.d.ts +3 -0
  77. package/dist/types/session/agent-session.d.ts +33 -0
  78. package/dist/types/system-prompt.d.ts +2 -0
  79. package/dist/types/task/executor.d.ts +2 -0
  80. package/dist/types/task/render.d.ts +5 -1
  81. package/dist/types/tiny/device.d.ts +78 -0
  82. package/dist/types/tiny/dtype.d.ts +85 -0
  83. package/dist/types/tiny/models.d.ts +185 -0
  84. package/dist/types/tiny/text.d.ts +19 -0
  85. package/dist/types/tiny/title-client.d.ts +32 -0
  86. package/dist/types/tiny/title-protocol.d.ts +74 -0
  87. package/dist/types/tiny/worker.d.ts +2 -0
  88. package/dist/types/tools/bash.d.ts +3 -2
  89. package/dist/types/tools/eval.d.ts +1 -1
  90. package/dist/types/tools/index.d.ts +7 -4
  91. package/dist/types/tools/memory-edit.d.ts +40 -0
  92. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  93. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  94. package/dist/types/tools/memory-render.d.ts +60 -0
  95. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  96. package/dist/types/tools/todo-write.d.ts +8 -0
  97. package/dist/types/tools/tool-result.d.ts +2 -0
  98. package/dist/types/tui/code-cell.d.ts +2 -0
  99. package/dist/types/tui/output-block.d.ts +17 -0
  100. package/dist/types/utils/title-generator.d.ts +3 -0
  101. package/package.json +18 -14
  102. package/scripts/build-binary.ts +1 -0
  103. package/src/capability/rule-buckets.ts +64 -0
  104. package/src/capability/rule.ts +8 -0
  105. package/src/cli/completion-gen.ts +550 -0
  106. package/src/cli/setup-cli.ts +5 -3
  107. package/src/cli/tiny-models-cli.ts +127 -0
  108. package/src/cli-commands.ts +3 -0
  109. package/src/cli.ts +9 -15
  110. package/src/commands/complete.ts +66 -0
  111. package/src/commands/completions.ts +60 -0
  112. package/src/commands/setup.ts +29 -4
  113. package/src/commands/tiny-models.ts +36 -0
  114. package/src/config/model-equivalence.ts +43 -2
  115. package/src/config/model-id-affixes.ts +64 -0
  116. package/src/config/model-registry.ts +84 -10
  117. package/src/config/settings-schema.ts +275 -15
  118. package/src/discovery/builtin-defaults.ts +39 -0
  119. package/src/discovery/builtin-rules/index.ts +48 -0
  120. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  121. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  122. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  123. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  124. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  125. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  126. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  127. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  128. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  129. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  130. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  131. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  132. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  133. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  134. package/src/discovery/index.ts +1 -0
  135. package/src/edit/hashline/block-resolver.ts +14 -0
  136. package/src/edit/hashline/diff.ts +9 -8
  137. package/src/edit/hashline/execute.ts +2 -1
  138. package/src/edit/hashline/index.ts +1 -0
  139. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  140. package/src/eval/js/shared/local-module-loader.ts +13 -1
  141. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  142. package/src/eval/py/kernel.ts +37 -15
  143. package/src/eval/py/runtime.ts +57 -28
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/export/html/template.js +0 -12
  146. package/src/export/ttsr.ts +2 -0
  147. package/src/internal-urls/agent-protocol.ts +18 -1
  148. package/src/internal-urls/artifact-protocol.ts +19 -1
  149. package/src/internal-urls/docs-index.generated.ts +8 -7
  150. package/src/internal-urls/local-protocol.ts +14 -1
  151. package/src/internal-urls/memory-protocol.ts +6 -1
  152. package/src/internal-urls/omp-protocol.ts +5 -1
  153. package/src/internal-urls/router.ts +20 -1
  154. package/src/internal-urls/rule-protocol.ts +8 -1
  155. package/src/internal-urls/skill-protocol.ts +8 -1
  156. package/src/internal-urls/types.ts +27 -0
  157. package/src/lsp/render.ts +1 -1
  158. package/src/main.ts +18 -1
  159. package/src/mcp/oauth-flow.ts +2 -2
  160. package/src/memory-backend/index.ts +1 -0
  161. package/src/memory-backend/resolve.ts +4 -1
  162. package/src/memory-backend/types.ts +8 -1
  163. package/src/mnemosyne/backend.ts +374 -0
  164. package/src/mnemosyne/config.ts +160 -0
  165. package/src/mnemosyne/index.ts +3 -0
  166. package/src/mnemosyne/state.ts +548 -0
  167. package/src/modes/acp/acp-agent.ts +11 -6
  168. package/src/modes/components/agent-dashboard.ts +4 -4
  169. package/src/modes/components/custom-editor.ts +3 -2
  170. package/src/modes/components/diff.ts +2 -2
  171. package/src/modes/components/extensions/extension-list.ts +3 -2
  172. package/src/modes/components/footer.ts +5 -6
  173. package/src/modes/components/history-search.ts +3 -3
  174. package/src/modes/components/hook-selector.ts +92 -8
  175. package/src/modes/components/index.ts +2 -0
  176. package/src/modes/components/mcp-add-wizard.ts +3 -3
  177. package/src/modes/components/model-selector.ts +5 -4
  178. package/src/modes/components/oauth-selector.ts +3 -3
  179. package/src/modes/components/segment-track.ts +52 -0
  180. package/src/modes/components/session-observer-overlay.ts +19 -13
  181. package/src/modes/components/session-selector.ts +3 -3
  182. package/src/modes/components/settings-defs.ts +7 -0
  183. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  184. package/src/modes/components/status-line/segments.ts +2 -2
  185. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  186. package/src/modes/components/tips.txt +13 -0
  187. package/src/modes/components/tool-execution.ts +72 -4
  188. package/src/modes/components/tree-selector.ts +3 -3
  189. package/src/modes/components/user-message-selector.ts +3 -3
  190. package/src/modes/components/welcome.ts +102 -43
  191. package/src/modes/controllers/command-controller.ts +16 -1
  192. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  193. package/src/modes/controllers/input-controller.ts +69 -21
  194. package/src/modes/gradient-highlight.ts +70 -0
  195. package/src/modes/interactive-mode.ts +75 -114
  196. package/src/modes/internal-url-autocomplete.ts +143 -0
  197. package/src/modes/orchestrate.ts +36 -0
  198. package/src/modes/prompt-action-autocomplete.ts +12 -0
  199. package/src/modes/setup-wizard/index.ts +88 -0
  200. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  201. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  202. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  204. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  205. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  206. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  207. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  208. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  209. package/src/modes/theme/shimmer.ts +5 -0
  210. package/src/modes/theme/theme.ts +44 -20
  211. package/src/modes/types.ts +6 -1
  212. package/src/modes/ultrathink.ts +9 -53
  213. package/src/modes/utils/keybinding-matchers.ts +11 -0
  214. package/src/prompts/system/memory-consolidation-system.md +8 -0
  215. package/src/prompts/system/memory-extraction-system.md +26 -0
  216. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +6 -17
  217. package/src/prompts/system/system-prompt.md +2 -0
  218. package/src/prompts/system/tiny-title-system.md +8 -0
  219. package/src/prompts/tools/memory-edit.md +8 -0
  220. package/src/prompts/tools/read.md +4 -0
  221. package/src/prompts/tools/task.md +4 -7
  222. package/src/sdk.ts +13 -21
  223. package/src/session/agent-session.ts +128 -44
  224. package/src/slash-commands/builtin-registry.ts +18 -1
  225. package/src/system-prompt.ts +4 -0
  226. package/src/task/commands.ts +1 -5
  227. package/src/task/executor.ts +8 -0
  228. package/src/task/index.ts +2 -0
  229. package/src/task/render.ts +69 -26
  230. package/src/tiny/device.ts +117 -0
  231. package/src/tiny/dtype.ts +101 -0
  232. package/src/tiny/models.ts +218 -0
  233. package/src/tiny/text.ts +54 -0
  234. package/src/tiny/title-client.ts +395 -0
  235. package/src/tiny/title-protocol.ts +51 -0
  236. package/src/tiny/worker.ts +587 -0
  237. package/src/tools/bash.ts +74 -29
  238. package/src/tools/browser/tab-worker.ts +1 -1
  239. package/src/tools/eval.ts +9 -4
  240. package/src/tools/index.ts +17 -22
  241. package/src/tools/memory-edit.ts +59 -0
  242. package/src/tools/memory-recall.ts +100 -0
  243. package/src/tools/memory-reflect.ts +88 -0
  244. package/src/tools/memory-render.ts +185 -0
  245. package/src/tools/memory-retain.ts +91 -0
  246. package/src/tools/read.ts +1 -0
  247. package/src/tools/renderers.ts +4 -2
  248. package/src/tools/todo-write.ts +128 -29
  249. package/src/tools/tool-result.ts +8 -0
  250. package/src/tui/code-cell.ts +6 -1
  251. package/src/tui/output-block.ts +199 -38
  252. package/src/utils/title-generator.ts +115 -13
  253. package/dist/types/tools/recipe/index.d.ts +0 -46
  254. package/dist/types/tools/recipe/render.d.ts +0 -36
  255. package/dist/types/tools/recipe/runner.d.ts +0 -60
  256. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  257. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  258. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  259. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  260. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  261. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  262. package/src/prompts/tools/recipe.md +0 -16
  263. package/src/tools/hindsight-recall.ts +0 -69
  264. package/src/tools/hindsight-reflect.ts +0 -58
  265. package/src/tools/hindsight-retain.ts +0 -57
  266. package/src/tools/recipe/index.ts +0 -81
  267. package/src/tools/recipe/render.ts +0 -19
  268. package/src/tools/recipe/runner.ts +0 -219
  269. package/src/tools/recipe/runners/cargo.ts +0 -131
  270. package/src/tools/recipe/runners/index.ts +0 -8
  271. package/src/tools/recipe/runners/just.ts +0 -73
  272. package/src/tools/recipe/runners/make.ts +0 -101
  273. package/src/tools/recipe/runners/pkg.ts +0 -167
  274. package/src/tools/recipe/runners/task.ts +0 -72
@@ -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,10 +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";
27
+ import { renderSegmentTrack } from "./segment-track";
28
+
29
+ /** One segment of a {@link HookSelectorSlider} — a label, its accent color, and
30
+ * an optional detail line (e.g. the resolved model name) shown beneath the
31
+ * track while the segment is active. */
32
+ export interface HookSelectorSliderSegment {
33
+ label: string;
34
+ /** Theme color for the segment label; defaults to `accent`. */
35
+ color?: ThemeColor;
36
+ /** Secondary line rendered under the track when this segment is selected. */
37
+ detail?: string;
38
+ }
39
+
40
+ /**
41
+ * A horizontal left/right selector rendered above the option list. Unlike the
42
+ * up/down option cursor, the slider is moved with the left/right arrows from
43
+ * any list position, letting the caller capture an orthogonal choice (e.g. the
44
+ * model tier to continue execution with) alongside the selected option.
45
+ */
46
+ export interface HookSelectorSlider {
47
+ /** Dim caption rendered before the slider track (e.g. "continue with"). */
48
+ caption?: string;
49
+ segments: HookSelectorSliderSegment[];
50
+ /** Initially highlighted segment index. */
51
+ index: number;
52
+ /** Invoked with the new index whenever the slider moves. */
53
+ onChange?: (index: number) => void;
54
+ }
22
55
 
23
56
  export interface HookSelectorOptions {
24
57
  tui?: TUI;
@@ -31,6 +64,7 @@ export interface HookSelectorOptions {
31
64
  onRight?: () => void;
32
65
  onExternalEditor?: () => void;
33
66
  helpText?: string;
67
+ slider?: HookSelectorSlider;
34
68
  }
35
69
 
36
70
  class OutlinedList extends Container {
@@ -69,6 +103,9 @@ export class HookSelectorComponent extends Container {
69
103
  #onLeftCallback: (() => void) | undefined;
70
104
  #onRightCallback: (() => void) | undefined;
71
105
  #onExternalEditorCallback: (() => void) | undefined;
106
+ #slider: HookSelectorSlider | undefined;
107
+ #sliderIndex: number = 0;
108
+ #sliderComponent: Text | undefined;
72
109
  constructor(
73
110
  title: string,
74
111
  options: string[],
@@ -87,6 +124,10 @@ export class HookSelectorComponent extends Container {
87
124
  this.#onLeftCallback = opts?.onLeft;
88
125
  this.#onRightCallback = opts?.onRight;
89
126
  this.#onExternalEditorCallback = opts?.onExternalEditor;
127
+ if (opts?.slider && opts.slider.segments.length > 0) {
128
+ this.#slider = opts.slider;
129
+ this.#sliderIndex = Math.max(0, Math.min(opts.slider.index, opts.slider.segments.length - 1));
130
+ }
90
131
 
91
132
  this.addChild(new DynamicBorder());
92
133
  this.addChild(new Spacer(1));
@@ -95,6 +136,12 @@ export class HookSelectorComponent extends Container {
95
136
  this.addChild(this.#titleComponent);
96
137
  this.addChild(new Spacer(1));
97
138
 
139
+ if (this.#slider) {
140
+ this.#sliderComponent = new Text(this.#renderSliderLine(), 1, 0);
141
+ this.addChild(this.#sliderComponent);
142
+ this.addChild(new Spacer(1));
143
+ }
144
+
98
145
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
99
146
  this.#countdown = new CountdownTimer(
100
147
  opts.timeout,
@@ -160,23 +207,60 @@ export class HookSelectorComponent extends Container {
160
207
  }
161
208
  }
162
209
 
210
+ /** Render the slider block in the style of the status line: each option is a
211
+ * distinctly colored segment, the active one filled as a powerline chip
212
+ * (its accent as the background, a luminance-matched label, flanked by
213
+ * triangle caps) and the rest shown as plain colored labels joined by a thin
214
+ * separator. Edge arrows brighten while there is room to move. When the
215
+ * active segment carries a `detail` (e.g. the resolved model name) a muted
216
+ * second line is appended. Returns one or two `\n`-joined lines. */
217
+ #renderSliderLine(): string {
218
+ const slider = this.#slider;
219
+ if (!slider) return "";
220
+ const segments = slider.segments;
221
+ const active = this.#sliderIndex;
222
+ const track = renderSegmentTrack(segments, active);
223
+
224
+ const leftArrow = theme.fg(active > 0 ? "accent" : "dim", "◂");
225
+ const rightArrow = theme.fg(active < segments.length - 1 ? "accent" : "dim", "▸");
226
+ const caption = slider.caption ? `${theme.fg("dim", slider.caption)} ` : "";
227
+ const trackLine = `${caption}${leftArrow} ${track} ${rightArrow}`;
228
+ const detail = segments[active]?.detail;
229
+ if (!detail) return trackLine;
230
+ return `${trackLine}\n ${theme.fg("dim", "↳")} ${theme.fg("muted", detail)}`;
231
+ }
232
+
233
+ /** Move the slider by `delta`, clamped to the segment range, refresh the
234
+ * rendered track, and notify the caller only when the index actually moves. */
235
+ #moveSlider(delta: number): void {
236
+ const slider = this.#slider;
237
+ if (!slider) return;
238
+ const next = Math.max(0, Math.min(slider.segments.length - 1, this.#sliderIndex + delta));
239
+ if (next === this.#sliderIndex) return;
240
+ this.#sliderIndex = next;
241
+ this.#sliderComponent?.setText(this.#renderSliderLine());
242
+ slider.onChange?.(next);
243
+ }
244
+
163
245
  handleInput(keyData: string): void {
164
246
  // Reset countdown on any interaction
165
247
  this.#countdown?.reset();
166
248
 
167
- if (matchesKey(keyData, "up") || keyData === "k") {
249
+ if (matchesSelectUp(keyData) || keyData === "k") {
168
250
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
169
251
  this.#updateList();
170
- } else if (matchesKey(keyData, "down") || keyData === "j") {
252
+ } else if (matchesSelectDown(keyData) || keyData === "j") {
171
253
  this.#selectedIndex = Math.min(this.#options.length - 1, this.#selectedIndex + 1);
172
254
  this.#updateList();
173
255
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
174
256
  const selected = this.#options[this.#selectedIndex];
175
257
  if (selected) this.#onSelectCallback(selected);
176
- } else if (matchesKey(keyData, "left")) {
177
- this.#onLeftCallback?.();
178
- } else if (matchesKey(keyData, "right")) {
179
- this.#onRightCallback?.();
258
+ } else if (matchesKey(keyData, "left") || (this.#slider && keyData === "h")) {
259
+ if (this.#slider) this.#moveSlider(-1);
260
+ else this.#onLeftCallback?.();
261
+ } else if (matchesKey(keyData, "right") || (this.#slider && keyData === "l")) {
262
+ if (this.#slider) this.#moveSlider(1);
263
+ else this.#onRightCallback?.();
180
264
  } else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
181
265
  this.#onExternalEditorCallback();
182
266
  } else if (matchesSelectCancel(keyData)) {
@@ -20,12 +20,14 @@ export * from "./model-selector";
20
20
  export * from "./oauth-selector";
21
21
  export * from "./queue-mode-selector";
22
22
  export * from "./read-tool-group";
23
+ export * from "./segment-track";
23
24
  export * from "./session-selector";
24
25
  export * from "./settings-selector";
25
26
  export * from "./show-images-selector";
26
27
  export * from "./status-line";
27
28
  export * from "./theme-selector";
28
29
  export * from "./thinking-selector";
30
+ export * from "./tiny-title-download-progress";
29
31
  export * from "./todo-reminder";
30
32
  export * from "./tool-execution";
31
33
  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
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared renderer for a horizontal row of colored "segments" styled after the
3
+ * status line: each segment shows in its own accent, the active one is filled
4
+ * as a powerline chip (its accent as the background, a luminance-matched label,
5
+ * flanked by triangle caps) and the rest are plain colored labels joined by a
6
+ * thin separator.
7
+ *
8
+ * Used by the plan-mode model-tier slider ({@link HookSelectorComponent}) and
9
+ * the ctrl+p role-cycle status so both surfaces read identically.
10
+ */
11
+ import { type ThemeColor, theme } from "../theme/theme";
12
+
13
+ export interface TrackSegment {
14
+ label: string;
15
+ /** Theme color for the segment; defaults to `accent`. */
16
+ color?: ThemeColor;
17
+ }
18
+
19
+ const FG_RESET = "\x1b[39m";
20
+ const BG_RESET = "\x1b[49m";
21
+
22
+ /**
23
+ * Render `segments` as a colored chip track with `activeIndex` filled. Returns
24
+ * a single line of styled text with no surrounding caption or arrows — callers
25
+ * frame it as they need.
26
+ */
27
+ export function renderSegmentTrack(segments: TrackSegment[], activeIndex: number): string {
28
+ // Powerline triangles point *into* the chip so the colored caps merge with
29
+ // the filled body: left cap points left, right cap points right.
30
+ const capLeft = theme.sep.powerlineRight;
31
+ const capRight = theme.sep.powerlineLeft;
32
+ const thinSep = theme.fg("statusLineSep", theme.sep.powerlineThin);
33
+
34
+ let track = "";
35
+ segments.forEach((segment, i) => {
36
+ if (i > 0) {
37
+ // A thin separator reads cleanly only between two plain labels; the chip
38
+ // caps already delimit the active segment, so pad around it instead.
39
+ track += i === activeIndex || i - 1 === activeIndex ? " " : ` ${thinSep} `;
40
+ }
41
+ const color = segment.color ?? "accent";
42
+ const fg = theme.getFgAnsi(color);
43
+ if (i !== activeIndex) {
44
+ track += `${fg}${segment.label}${FG_RESET}`;
45
+ return;
46
+ }
47
+ const bg = fg.replace("\x1b[38;", "\x1b[48;");
48
+ const label = `${bg}${theme.getContrastFgAnsi(color)}\x1b[1m ${segment.label} \x1b[22m${BG_RESET}`;
49
+ track += `${fg}${capLeft}${label}${fg}${capRight}${FG_RESET}`;
50
+ });
51
+ return track;
52
+ }
@@ -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,13 @@
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
13
+ Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc