@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
@@ -31,6 +31,7 @@ import {
31
31
  } from "../../tools/json-tree";
32
32
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
33
33
  import { toolRenderers } from "../../tools/renderers";
34
+ import { TODO_WRITE_STRIKE_TOTAL_FRAMES } from "../../tools/todo-write";
34
35
  import { renderStatusLine } from "../../tui";
35
36
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
36
37
  import { renderDiff } from "./diff";
@@ -163,6 +164,8 @@ export class ToolExecutionComponent extends Container {
163
164
  // Spinner animation for partial task results
164
165
  #spinnerFrame?: number;
165
166
  #spinnerInterval?: NodeJS.Timeout;
167
+ // Todo write completion strikethrough reveal animation
168
+ #todoStrikeInterval?: NodeJS.Timeout;
166
169
  // Track if args are still being streamed (for edit/write spinner)
167
170
  #argsComplete = false;
168
171
  #renderState: {
@@ -251,12 +254,24 @@ export class ToolExecutionComponent extends Container {
251
254
  effectiveArgs = args;
252
255
  }
253
256
 
254
- // Coalesce duplicate computes for identical args.
257
+ // Coalesce duplicate computes for identical args. The key pairs the
258
+ // streaming flag with a content hash: the final (args-complete) pass
259
+ // computes an untrimmed diff and must run even when the payload is
260
+ // byte-identical to the last streamed chunk — only `isStreaming` differs,
261
+ // and it flips the trailing-line trim. Without the flag a single-line edit
262
+ // whose trailing payload line never gets a newline stays stuck on the
263
+ // trimmed "no changes" streaming preview and renders no diff. Hashing keeps
264
+ // the retained key tiny instead of holding the whole serialized blob.
265
+ const streamingState = this.#argsComplete ? "final" : "stream";
255
266
  let argsKey: string;
256
267
  try {
257
- argsKey = JSON.stringify(effectiveArgs);
268
+ argsKey = `${streamingState}:${Bun.hash(JSON.stringify(effectiveArgs))}`;
258
269
  } catch {
259
- argsKey = String(Date.now());
270
+ // effectiveArgs isn't JSON-serializable (exotic value in tool args).
271
+ // The raw streamed JSON is a plain string, so hash that instead of a
272
+ // timestamp — a deterministic key keeps the dedup cache working
273
+ // instead of recomputing (and re-reading the file) on every render.
274
+ argsKey = `${streamingState}:partial:${Bun.hash(partialJson ?? "")}`;
260
275
  }
261
276
  if (argsKey === this.#editDiffLastArgsKey) return;
262
277
  this.#editDiffLastArgsKey = argsKey;
@@ -304,6 +319,7 @@ export class ToolExecutionComponent extends Container {
304
319
  this.#argsComplete = true;
305
320
  }
306
321
  this.#updateSpinnerAnimation();
322
+ this.#updateTodoStrikeAnimation();
307
323
  this.#updateDisplay();
308
324
  // Convert non-PNG images to PNG for Kitty protocol (async)
309
325
  this.#maybeConvertImagesForKitty();
@@ -379,6 +395,43 @@ export class ToolExecutionComponent extends Container {
379
395
  }
380
396
  }
381
397
 
398
+ #updateTodoStrikeAnimation(): void {
399
+ if (this.#toolName !== "todo_write" || this.#isPartial || this.#result?.isError) {
400
+ this.#stopTodoStrikeAnimation();
401
+ return;
402
+ }
403
+ const completedTasks = (this.#result?.details as { completedTasks?: unknown[] } | undefined)?.completedTasks;
404
+ if (!completedTasks || completedTasks.length === 0) {
405
+ this.#stopTodoStrikeAnimation();
406
+ return;
407
+ }
408
+ if (this.#todoStrikeInterval) return;
409
+
410
+ this.#spinnerFrame = 0;
411
+ this.#renderState.spinnerFrame = 0;
412
+ this.#todoStrikeInterval = setInterval(() => {
413
+ const nextFrame = (this.#spinnerFrame ?? 0) + 1;
414
+ if (nextFrame > TODO_WRITE_STRIKE_TOTAL_FRAMES) {
415
+ this.#stopTodoStrikeAnimation();
416
+ } else {
417
+ this.#spinnerFrame = nextFrame;
418
+ this.#renderState.spinnerFrame = nextFrame;
419
+ }
420
+ this.#ui.requestRender();
421
+ }, 65);
422
+ }
423
+
424
+ #stopTodoStrikeAnimation(): void {
425
+ if (this.#todoStrikeInterval) {
426
+ clearInterval(this.#todoStrikeInterval);
427
+ this.#todoStrikeInterval = undefined;
428
+ }
429
+ if (!this.#spinnerInterval) {
430
+ this.#spinnerFrame = undefined;
431
+ this.#renderState.spinnerFrame = undefined;
432
+ }
433
+ }
434
+
382
435
  /**
383
436
  * Stop spinner animation and cleanup resources.
384
437
  */
@@ -388,6 +441,7 @@ export class ToolExecutionComponent extends Container {
388
441
  this.#spinnerInterval = undefined;
389
442
  this.#spinnerFrame = undefined;
390
443
  }
444
+ this.#stopTodoStrikeAnimation();
391
445
  this.#editDiffAbort?.abort();
392
446
  this.#editDiffAbort = undefined;
393
447
  }
@@ -428,6 +482,11 @@ export class ToolExecutionComponent extends Container {
428
482
  const inline = Boolean((tool as { inline?: boolean }).inline);
429
483
  this.#contentBox.setBgFn(inline ? undefined : bgFn);
430
484
  this.#contentBox.clear();
485
+ // Mirror the built-in renderer branch so custom renderers (notably the
486
+ // task tool, whose live instance routes through here) receive the same
487
+ // render context — e.g. the `hasResult` flag that suppresses the task
488
+ // call preview once result lines exist.
489
+ this.#renderState.renderContext = this.#buildRenderContext();
431
490
 
432
491
  // Render call component
433
492
  const shouldRenderCall = !this.#result || !mergeCallAndResult;
@@ -696,6 +755,11 @@ export class ToolExecutionComponent extends Container {
696
755
  context.output = output;
697
756
  context.expanded = this.#expanded;
698
757
  context.previewLines = EVAL_DEFAULT_PREVIEW_LINES;
758
+ } else if (this.#toolName === "task") {
759
+ // Once a result snapshot exists the task renderer's `renderResult`
760
+ // draws every dispatched agent as a progress/result line, so tell
761
+ // `renderCall` to drop its duplicate streaming preview list.
762
+ context.hasResult = Boolean(this.#result);
699
763
  } else if (isEditLikeToolName(this.#toolName)) {
700
764
  context.editMode = this.#editMode;
701
765
  const previews = this.#editDiffPreview;
@@ -12,7 +12,7 @@ import {
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import type { TreeFilterMode } from "../../config/settings-schema";
14
14
  import { theme } from "../../modes/theme/theme";
15
- import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
15
+ import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
16
16
  import type { SessionTreeNode } from "../../session/session-manager";
17
17
  import { shortenPath } from "../../tools/render-utils";
18
18
  import { toPathList } from "../../tools/search";
@@ -718,9 +718,9 @@ class TreeList implements Component {
718
718
  }
719
719
 
720
720
  handleInput(keyData: string): void {
721
- if (matchesKey(keyData, "up")) {
721
+ if (matchesSelectUp(keyData)) {
722
722
  this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredNodes.length - 1 : this.#selectedIndex - 1;
723
- } else if (matchesKey(keyData, "down")) {
723
+ } else if (matchesSelectDown(keyData)) {
724
724
  this.#selectedIndex = this.#selectedIndex === this.#filteredNodes.length - 1 ? 0 : this.#selectedIndex + 1;
725
725
  } else if (matchesKey(keyData, "left")) {
726
726
  // Page up
@@ -1,6 +1,6 @@
1
1
  import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
2
2
  import { theme } from "../../modes/theme/theme";
3
- import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
3
+ import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
4
4
  import { DynamicBorder } from "./dynamic-border";
5
5
 
6
6
  interface UserMessageItem {
@@ -78,11 +78,11 @@ class UserMessageList implements Component {
78
78
 
79
79
  handleInput(keyData: string): void {
80
80
  // Up arrow - go to previous (older) message, wrap to bottom when at top
81
- if (matchesKey(keyData, "up")) {
81
+ if (matchesSelectUp(keyData)) {
82
82
  this.#selectedIndex = this.#selectedIndex === 0 ? this.messages.length - 1 : this.#selectedIndex - 1;
83
83
  }
84
84
  // Down arrow - go to next (newer) message, wrap to top when at bottom
85
- else if (matchesKey(keyData, "down")) {
85
+ else if (matchesSelectDown(keyData)) {
86
86
  this.#selectedIndex = this.#selectedIndex === this.messages.length - 1 ? 0 : this.#selectedIndex + 1;
87
87
  }
88
88
  // Enter - select message and branch
@@ -1,6 +1,45 @@
1
- import { type Component, padding, TERMINAL, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import {
2
+ type Component,
3
+ padding,
4
+ replaceTabs,
5
+ TERMINAL,
6
+ truncateToWidth,
7
+ visibleWidth,
8
+ wrapTextWithAnsi,
9
+ } from "@oh-my-pi/pi-tui";
2
10
  import { APP_NAME } from "@oh-my-pi/pi-utils";
3
11
  import { theme } from "../../modes/theme/theme";
12
+ import tipsText from "./tips.txt" with { type: "text" };
13
+
14
+ /** Tips embedded at build time, one per line; blanks dropped. */
15
+ const TIPS: readonly string[] = tipsText
16
+ .split("\n")
17
+ .map(line => line.trim())
18
+ .filter(line => line.length > 0);
19
+
20
+ export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
21
+ const label = "Tip: ";
22
+ const labelWidth = visibleWidth(label);
23
+ const bodyBudget = boxWidth - 1 - labelWidth; // 1 = leading indent
24
+ if (bodyBudget < 8) return [];
25
+
26
+ const wrappedBody = wrapTextWithAnsi(replaceTabs(tip), bodyBudget);
27
+ if (wrappedBody.length === 0) return [];
28
+
29
+ const encoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
30
+ const purple = Bun.color("#b48cff", encoding) ?? "";
31
+ const lightBlue = Bun.color("#9ccfff", encoding) ?? "";
32
+ const italic = "\x1b[3m";
33
+ const dim = "\x1b[2m";
34
+ const reset = "\x1b[0m";
35
+ const continuationIndent = padding(labelWidth);
36
+
37
+ return wrappedBody.map((body, index) =>
38
+ index === 0
39
+ ? ` ${italic}${purple}${label}${dim}${lightBlue}${body}${reset}`
40
+ : ` ${italic}${continuationIndent}${dim}${lightBlue}${body}${reset}`,
41
+ );
42
+ }
4
43
 
5
44
  export interface RecentSession {
6
45
  name: string;
@@ -19,6 +58,8 @@ export interface LspServerInfo {
19
58
  export class WelcomeComponent implements Component {
20
59
  #animStart: number | null = null;
21
60
  #animTimer: ReturnType<typeof setInterval> | null = null;
61
+ /** Tip chosen once per instance so re-renders (intro, LSP updates) don't shuffle it. */
62
+ readonly #tip: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
22
63
 
23
64
  constructor(
24
65
  private readonly version: string,
@@ -212,9 +253,22 @@ export class WelcomeComponent implements Component {
212
253
  lines.push(bl + h.repeat(leftCol) + br);
213
254
  }
214
255
 
256
+ // Randomly picked tip, rendered directly beneath the box.
257
+ lines.push(...this.#renderTip(boxWidth));
258
+
215
259
  return lines;
216
260
  }
217
261
 
262
+ /**
263
+ * Render the per-instance tip line: a purple "Tip:" label followed by the
264
+ * tip body in dimmed light blue, the whole line italicized. Returns `[]`
265
+ * when no tip is available or the box is too narrow to be useful.
266
+ */
267
+ #renderTip(boxWidth: number): string[] {
268
+ if (!this.#tip) return [];
269
+ return renderWelcomeTip(this.#tip, boxWidth);
270
+ }
271
+
218
272
  /** Center text within a given width */
219
273
  #centerText(text: string, width: number): string {
220
274
  const visLen = visibleWidth(text);
@@ -620,12 +620,27 @@ export class CommandController {
620
620
  return;
621
621
  }
622
622
 
623
+ if (action === "stats" || action === "diagnose") {
624
+ const hook = action === "stats" ? backend.stats : backend.diagnose;
625
+ try {
626
+ const payload = await hook?.(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
627
+ if (!payload) {
628
+ this.ctx.showWarning(`Memory ${action} is not available for the ${backend.id} backend.`);
629
+ return;
630
+ }
631
+ showMarkdownPanel(this.ctx, `Memory ${action === "stats" ? "Stats" : "Diagnostics"}`, payload);
632
+ } catch (error) {
633
+ this.ctx.showError(`Memory ${action} failed: ${error instanceof Error ? error.message : String(error)}`);
634
+ }
635
+ return;
636
+ }
637
+
623
638
  if (action === "mm") {
624
639
  await this.#handleMentalModelsSubcommand(argumentText);
625
640
  return;
626
641
  }
627
642
 
628
- this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
643
+ this.ctx.showError("Usage: /memory <view|stats|diagnose|clear|reset|enqueue|rebuild|mm ...>");
629
644
  }
630
645
 
631
646
  async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
@@ -19,7 +19,7 @@ import type {
19
19
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
20
20
  import { HookEditorComponent } from "../../modes/components/hook-editor";
21
21
  import { HookInputComponent } from "../../modes/components/hook-input";
22
- import { HookSelectorComponent } from "../../modes/components/hook-selector";
22
+ import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
23
23
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
24
24
  import type { InteractiveModeContext } from "../../modes/types";
25
25
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
@@ -583,6 +583,7 @@ export class ExtensionUiController {
583
583
  title: string,
584
584
  options: string[],
585
585
  dialogOptions?: ExtensionUIDialogOptions,
586
+ extra?: { slider?: HookSelectorSlider },
586
587
  ): Promise<string | undefined> {
587
588
  const { promise, finish, attachAbort } = this.#createHookDialogState(
588
589
  () => this.hideHookSelector(),
@@ -623,6 +624,7 @@ export class ExtensionUiController {
623
624
  tui: this.ctx.ui,
624
625
  outline: dialogOptions?.outline,
625
626
  maxVisible,
627
+ slider: extra?.slider,
626
628
  },
627
629
  );
628
630
  this.ctx.editorContainer.clear();
@@ -3,6 +3,7 @@ import { type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
3
  import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
4
4
  import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
5
5
  import { isSettingsInitialized, settings } from "../../config/settings";
6
+ import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
6
7
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
7
8
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
8
9
  import { theme } from "../../modes/theme/theme";
@@ -10,6 +11,9 @@ import type { InteractiveModeContext } from "../../modes/types";
10
11
  import type { AgentSessionEvent } from "../../session/agent-session";
11
12
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
12
13
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
14
+ import { isTinyTitleLocalModelKey } from "../../tiny/models";
15
+ import { tinyTitleClient } from "../../tiny/title-client";
16
+ import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
13
17
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
14
18
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
15
19
  import { ensureSupportedImageInput } from "../../utils/image-loading";
@@ -24,9 +28,61 @@ function isExpandable(obj: unknown): obj is Expandable {
24
28
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
25
29
  }
26
30
 
31
+ const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
32
+ // A cached model fires its file-load events in a short burst and then goes silent
33
+ // while onnxruntime builds the session; a genuine download keeps streaming progress
34
+ // events for seconds. Only reveal the bar once a still-incomplete event arrives after
35
+ // this grace window, so an already-downloaded model never flashes the bar.
36
+ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
37
+
27
38
  export class InputController {
28
39
  constructor(private ctx: InteractiveModeContext) {}
29
40
 
41
+ #showTinyTitleDownloadProgress(modelKey: string): void {
42
+ if (!isTinyTitleLocalModelKey(modelKey) || this.ctx.isBackgrounded) return;
43
+ const component = new TinyTitleDownloadProgressComponent(modelKey);
44
+ let added = false;
45
+ let disposed = false;
46
+ let removeTimer: NodeJS.Timeout | undefined;
47
+ const remove = (): void => {
48
+ if (disposed) return;
49
+ disposed = true;
50
+ unsubscribe();
51
+ if (removeTimer) {
52
+ clearTimeout(removeTimer);
53
+ removeTimer = undefined;
54
+ }
55
+ if (added) {
56
+ this.ctx.chatContainer.removeChild(component);
57
+ this.ctx.ui.requestRender();
58
+ }
59
+ };
60
+ const scheduleRemove = (): void => {
61
+ if (removeTimer) clearTimeout(removeTimer);
62
+ removeTimer = setTimeout(remove, TINY_TITLE_PROGRESS_DONE_TTL_MS);
63
+ removeTimer.unref?.();
64
+ };
65
+ let revealAt = 0;
66
+ const update = (event: TinyTitleProgressEvent): void => {
67
+ if (disposed || event.modelKey !== modelKey) return;
68
+ component.update(event);
69
+ if (revealAt === 0) revealAt = performance.now() + TINY_TITLE_PROGRESS_REVEAL_DELAY_MS;
70
+ const complete = component.isComplete();
71
+ // Reveal only for a download still in flight past the grace window. Cache hits
72
+ // either complete or fall silent (onnx init emits no events) before this fires.
73
+ if (!added && !complete && performance.now() >= revealAt) {
74
+ this.ctx.chatContainer.addChild(component);
75
+ added = true;
76
+ }
77
+ if (added) this.ctx.ui.requestRender();
78
+ if (complete) {
79
+ if (added) scheduleRemove();
80
+ else remove();
81
+ }
82
+ };
83
+ const unsubscribe = tinyTitleClient.onProgress(update);
84
+ }
85
+
30
86
  setupKeyHandlers(): void {
31
87
  this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
32
88
  this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
@@ -329,6 +385,7 @@ export class InputController {
329
385
  // Generate session title on first message
330
386
  const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
331
387
  if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
388
+ this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
332
389
  const registry = this.ctx.session.modelRegistry;
333
390
  generateSessionTitle(
334
391
  text,
@@ -0,0 +1,70 @@
1
+ import { theme } from "./theme/theme";
2
+
3
+ const FG_RESET = "\x1b[39m";
4
+
5
+ /** Declarative spec for {@link createGradientHighlighter}. */
6
+ export interface GradientHighlightSpec {
7
+ /** Cheap, stateless presence probe used to skip the boundary regex on most lines. Must be non-global. */
8
+ probe: RegExp;
9
+ /** Global, word-bounded match regex walked by `.replace`. */
10
+ highlight: RegExp;
11
+ /** Number of color stops swept across the gradient. */
12
+ stops: number;
13
+ /** Maps a normalized position `t` in [0, 1) to an HSL hue in degrees. */
14
+ hue: (t: number) => number;
15
+ /** HSL saturation percentage. Default 90. */
16
+ saturation?: number;
17
+ /** HSL lightness percentage. Default 62. */
18
+ lightness?: number;
19
+ }
20
+
21
+ /**
22
+ * Build a stateless highlighter that paints each standalone match of `highlight`
23
+ * with a smooth HSL gradient for editor display. The returned function adds only
24
+ * zero-width SGR escapes — the visible width is unchanged — and returns the input
25
+ * untouched when `probe` does not match. The palette is compiled lazily and
26
+ * memoized per active color mode.
27
+ */
28
+ export function createGradientHighlighter(spec: GradientHighlightSpec): (text: string) => string {
29
+ const { probe, highlight, stops, hue, saturation = 90, lightness = 62 } = spec;
30
+
31
+ let cachedMode: string | undefined;
32
+ let cachedPalette: readonly string[] | undefined;
33
+
34
+ /** Gradient foreground escapes for the active color mode, compiled once per mode. */
35
+ const palette = (): readonly string[] => {
36
+ const mode = theme.getColorMode();
37
+ if (cachedPalette && cachedMode === mode) return cachedPalette;
38
+ const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
39
+ const next: string[] = [];
40
+ for (let i = 0; i < stops; i++) {
41
+ next.push(Bun.color(`hsl(${Math.round(hue(i / stops))}, ${saturation}%, ${lightness}%)`, format) ?? "");
42
+ }
43
+ cachedMode = mode;
44
+ cachedPalette = next;
45
+ return next;
46
+ };
47
+
48
+ /** Paint each character of `word` with the next gradient stop, resetting fg after. */
49
+ const paint = (word: string): string => {
50
+ const stopsArr = palette();
51
+ const n = word.length;
52
+ let out = "";
53
+ let prev = "";
54
+ for (let i = 0; i < n; i++) {
55
+ const color = stopsArr[Math.floor((i / n) * stopsArr.length)] ?? stopsArr[0] ?? "";
56
+ // Coalesce consecutive characters that resolve to the same stop.
57
+ if (color !== prev) {
58
+ out += color;
59
+ prev = color;
60
+ }
61
+ out += word[i];
62
+ }
63
+ return `${out}${FG_RESET}`;
64
+ };
65
+
66
+ return (text: string): string => {
67
+ if (!probe.test(text)) return text;
68
+ return text.replace(highlight, paint);
69
+ };
70
+ }