@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
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Inline TUI renderers for the long-term memory tools (`retain`, `recall`,
3
+ * `reflect`).
4
+ *
5
+ * These keep the transcript terse — one status line plus, for `retain`, one
6
+ * `Remember: …` line per stored item — instead of the generic JSON arg tree,
7
+ * which exploded multi-line memory blobs into an unreadable wall.
8
+ */
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import { Text } from "@oh-my-pi/pi-tui";
11
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
+ import type { Theme } from "../modes/theme/theme";
13
+ import { Ellipsis, renderStatusLine, truncateToWidth } from "../tui";
14
+ import {
15
+ createCachedComponent,
16
+ formatErrorMessage,
17
+ formatExpandHint,
18
+ PREVIEW_LIMITS,
19
+ replaceTabs,
20
+ type ToolUIStatus,
21
+ } from "./render-utils";
22
+
23
+ // Each stored memory renders as `<bullet> <content>`; the bullet glyph comes
24
+ // from the active theme (`•` by default, a nerd-font dot under nerd themes).
25
+
26
+ interface RetainRenderArgs {
27
+ items?: Array<{ content?: string; context?: string }>;
28
+ }
29
+
30
+ interface QueryRenderArgs {
31
+ query?: string;
32
+ }
33
+
34
+ function retainContents(args: RetainRenderArgs | undefined): string[] {
35
+ return (args?.items ?? []).map(item => replaceTabs((item?.content ?? "").trim())).filter(line => line.length > 0);
36
+ }
37
+
38
+ function resultText(result: { content?: Array<{ type: string; text?: string }> }): string {
39
+ return (result.content?.find(c => c.type === "text")?.text ?? "").trim();
40
+ }
41
+
42
+ /** Single-line query header used by `recall`/`reflect` calls and results. */
43
+ function queryHeader(
44
+ title: string,
45
+ query: string | undefined,
46
+ icon: ToolUIStatus,
47
+ theme: Theme,
48
+ meta?: string[],
49
+ ): string {
50
+ const trimmed = replaceTabs((query ?? "").trim());
51
+ const description = trimmed ? truncateToWidth(trimmed, 80, Ellipsis.Unicode) : undefined;
52
+ return renderStatusLine({ icon, title, description, meta }, theme);
53
+ }
54
+
55
+ function retainComponent(contents: string[], header: string, getExpanded: () => boolean, theme: Theme): Component {
56
+ return createCachedComponent(getExpanded, (width, expanded) => {
57
+ const lines = [header];
58
+ const limit = expanded ? contents.length : PREVIEW_LIMITS.COLLAPSED_ITEMS;
59
+ const shown = contents.slice(0, limit);
60
+ const bullet = theme.format.bullet;
61
+ const contentWidth = Math.max(8, width - 2 - Bun.stringWidth(bullet) - 1);
62
+ for (const content of shown) {
63
+ const value = truncateToWidth(content, contentWidth, Ellipsis.Unicode);
64
+ lines.push(` ${theme.fg("muted", bullet)} ${theme.fg("toolOutput", value)}`);
65
+ }
66
+ const remaining = contents.length - shown.length;
67
+ if (remaining > 0) {
68
+ lines.push(` ${theme.fg("dim", `… ${remaining} more`)} ${formatExpandHint(theme, expanded, true)}`);
69
+ }
70
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
71
+ });
72
+ }
73
+
74
+ export const retainToolRenderer = {
75
+ inline: true,
76
+ mergeCallAndResult: true,
77
+ renderCall(args: RetainRenderArgs, options: RenderResultOptions, theme: Theme): Component {
78
+ const contents = retainContents(args);
79
+ const header = renderStatusLine({ icon: "pending", title: "Retain" }, theme);
80
+ return retainComponent(contents, header, () => options.expanded, theme);
81
+ },
82
+ renderResult(
83
+ result: { content: Array<{ type: string; text?: string }>; details?: { count?: number }; isError?: boolean },
84
+ options: RenderResultOptions,
85
+ theme: Theme,
86
+ args?: RetainRenderArgs,
87
+ ): Component {
88
+ if (result.isError) {
89
+ return new Text(formatErrorMessage(resultText(result) || "Retain failed", theme), 0, 0);
90
+ }
91
+ const contents = retainContents(args);
92
+ // `summary` is the tool's own "N memories stored/queued." line; drop the
93
+ // trailing period so it reads cleanly as a status meta segment.
94
+ const summary = resultText(result).replace(/\.$/, "");
95
+ const header = renderStatusLine(
96
+ { icon: "success", title: "Retain", meta: summary ? [summary] : undefined },
97
+ theme,
98
+ );
99
+ return retainComponent(contents, header, () => options.expanded, theme);
100
+ },
101
+ };
102
+
103
+ export const recallToolRenderer = {
104
+ inline: true,
105
+ mergeCallAndResult: true,
106
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
107
+ return new Text(queryHeader("Recall", args.query, "pending", theme), 0, 0);
108
+ },
109
+ renderResult(
110
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
111
+ options: RenderResultOptions,
112
+ theme: Theme,
113
+ args?: QueryRenderArgs,
114
+ ): Component {
115
+ if (result.isError) {
116
+ return new Text(formatErrorMessage(resultText(result) || "Recall failed", theme), 0, 0);
117
+ }
118
+ const text = resultText(result);
119
+ const match = text.match(/^Found (\d+) relevant/);
120
+ const found = match ? Number(match[1]) : 0;
121
+ const icon: ToolUIStatus = found > 0 ? "success" : "warning";
122
+ const meta = [found > 0 ? `${found} found` : "no matches"];
123
+ const header = queryHeader("Recall", args?.query, icon, theme, meta);
124
+ if (found === 0) {
125
+ return new Text(header, 0, 0);
126
+ }
127
+ // Collapsed view is the header alone; expand to inspect the recalled
128
+ // memories without dumping the whole block into the transcript.
129
+ const body = text.replace(/^[^\n]*\n+/, "");
130
+ return createCachedComponent(
131
+ () => options.expanded,
132
+ (width, expanded) => {
133
+ const lines = [header];
134
+ if (expanded) {
135
+ const bodyLines = body.split("\n").slice(0, PREVIEW_LIMITS.OUTPUT_EXPANDED);
136
+ for (const line of bodyLines) {
137
+ lines.push(` ${theme.fg("muted", replaceTabs(line))}`);
138
+ }
139
+ } else {
140
+ lines.push(` ${formatExpandHint(theme, false, true)}`);
141
+ }
142
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
143
+ },
144
+ );
145
+ },
146
+ };
147
+
148
+ export const reflectToolRenderer = {
149
+ inline: true,
150
+ mergeCallAndResult: true,
151
+ renderCall(args: QueryRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
152
+ return new Text(queryHeader("Reflect", args.query, "pending", theme), 0, 0);
153
+ },
154
+ renderResult(
155
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
156
+ options: RenderResultOptions,
157
+ theme: Theme,
158
+ args?: QueryRenderArgs,
159
+ ): Component {
160
+ if (result.isError) {
161
+ return new Text(formatErrorMessage(resultText(result) || "Reflect failed", theme), 0, 0);
162
+ }
163
+ const header = queryHeader("Reflect", args?.query, "success", theme);
164
+ const answer = resultText(result);
165
+ const answerLines = answer.split("\n").filter(line => line.trim().length > 0);
166
+ return createCachedComponent(
167
+ () => options.expanded,
168
+ (width, expanded) => {
169
+ const limit = expanded ? PREVIEW_LIMITS.OUTPUT_EXPANDED : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
170
+ const shown = answerLines.slice(0, limit);
171
+ const lines = [header];
172
+ for (const line of shown) {
173
+ lines.push(` ${theme.fg("toolOutput", replaceTabs(line))}`);
174
+ }
175
+ const remaining = answerLines.length - shown.length;
176
+ if (remaining > 0) {
177
+ lines.push(
178
+ ` ${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`,
179
+ );
180
+ }
181
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Omit));
182
+ },
183
+ );
184
+ },
185
+ };
@@ -0,0 +1,91 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import * as z from "zod/v4";
3
+ import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
4
+ import type { ToolSession } from ".";
5
+
6
+ const memoryRetainSchema = z.object({
7
+ items: z
8
+ .array(
9
+ z.object({
10
+ content: z.string().describe("information to remember"),
11
+ context: z.string().describe("source context").optional(),
12
+ }),
13
+ )
14
+ .min(1)
15
+ .describe("memories to retain"),
16
+ });
17
+
18
+ export type MemoryRetainParams = z.infer<typeof memoryRetainSchema>;
19
+ export class MemoryRetainTool implements AgentTool<typeof memoryRetainSchema> {
20
+ readonly name = "retain";
21
+ readonly approval = "read" as const;
22
+ readonly label = "Retain";
23
+ readonly description = retainDescription;
24
+ readonly parameters = memoryRetainSchema;
25
+ readonly strict = true;
26
+ readonly loadMode = "discoverable";
27
+ readonly summary = "Store important facts in long-term memory";
28
+
29
+ constructor(private readonly session: ToolSession) {}
30
+
31
+ static createIf(session: ToolSession): MemoryRetainTool | null {
32
+ const backend = session.settings.get("memory.backend");
33
+ if (backend !== "hindsight" && backend !== "mnemosyne") return null;
34
+ return new MemoryRetainTool(session);
35
+ }
36
+
37
+ async execute(_id: string, params: MemoryRetainParams): Promise<AgentToolResult> {
38
+ const backend = this.session.settings.get("memory.backend");
39
+ if (backend === "mnemosyne") {
40
+ const state = this.session.getMnemosyneSessionState?.();
41
+ if (!state) {
42
+ throw new Error("Mnemosyne backend is not initialised for this session.");
43
+ }
44
+
45
+ for (const item of params.items) {
46
+ state.rememberScoped(item.content, {
47
+ source: "coding-agent-retain",
48
+ importance: 0.75,
49
+ metadata: {
50
+ session_id: state.sessionId,
51
+ cwd: state.session.sessionManager.getCwd(),
52
+ context: item.context ?? null,
53
+ tool: "retain",
54
+ },
55
+ scope: "bank",
56
+ extract: true,
57
+ extractEntities: true,
58
+ veracity: "tool",
59
+ memoryType: "fact",
60
+ });
61
+ }
62
+
63
+ const count = params.items.length;
64
+ const noun = count === 1 ? "memory" : "memories";
65
+ return {
66
+ content: [{ type: "text", text: `${count} ${noun} stored.` }],
67
+ details: { count },
68
+ };
69
+ }
70
+
71
+ const state = this.session.getHindsightSessionState?.();
72
+ if (!state) {
73
+ throw new Error("Hindsight backend is not initialised for this session.");
74
+ }
75
+
76
+ // Push every item onto the session-owned queue and return immediately.
77
+ // The queue flushes either when it reaches its batch threshold or when
78
+ // its debounce timer fires. If the eventual batch fails, the queue
79
+ // surfaces a UI-only warning notice — the LLM is not informed.
80
+ for (const item of params.items) {
81
+ state.enqueueRetain(item.content, item.context);
82
+ }
83
+
84
+ const count = params.items.length;
85
+ const noun = count === 1 ? "memory" : "memories";
86
+ return {
87
+ content: [{ type: "text", text: `${count} ${noun} queued.` }],
88
+ details: { count },
89
+ };
90
+ }
91
+ }
package/src/tools/read.ts CHANGED
@@ -689,6 +689,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
689
689
  DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
690
690
  IS_HL_MODE: displayMode.hashLines,
691
691
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
692
+ INSPECT_IMAGE_ENABLED: this.#inspectImageEnabled,
692
693
  });
693
694
  }
694
695
 
@@ -22,8 +22,8 @@ import { findToolRenderer } from "./find";
22
22
  import { githubToolRenderer } from "./gh-renderer";
23
23
  import { inspectImageToolRenderer } from "./inspect-image-renderer";
24
24
  import { jobToolRenderer } from "./job";
25
+ import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
25
26
  import { readToolRenderer } from "./read";
26
- import { recipeToolRenderer } from "./recipe/render";
27
27
  import { resolveToolRenderer } from "./resolve";
28
28
  import { searchToolRenderer } from "./search";
29
29
  import { searchToolBm25Renderer } from "./search-tool-bm25";
@@ -50,7 +50,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
50
50
  ast_edit: astEditToolRenderer as ToolRenderer,
51
51
  bash: bashToolRenderer as ToolRenderer,
52
52
  browser: browserToolRenderer as ToolRenderer,
53
- recipe: recipeToolRenderer as ToolRenderer,
54
53
  debug: debugToolRenderer as ToolRenderer,
55
54
  eval: evalToolRenderer as ToolRenderer,
56
55
  edit: editToolRenderer as ToolRenderer,
@@ -62,6 +61,9 @@ export const toolRenderers: Record<string, ToolRenderer> = {
62
61
  read: readToolRenderer as ToolRenderer,
63
62
  job: jobToolRenderer as ToolRenderer,
64
63
  resolve: resolveToolRenderer as ToolRenderer,
64
+ retain: retainToolRenderer as ToolRenderer,
65
+ recall: recallToolRenderer as ToolRenderer,
66
+ reflect: reflectToolRenderer as ToolRenderer,
65
67
  search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
66
68
  ssh: sshToolRenderer as ToolRenderer,
67
69
  task: taskToolRenderer as ToolRenderer,
@@ -35,9 +35,15 @@ export interface TodoPhase {
35
35
  tasks: TodoItem[];
36
36
  }
37
37
 
38
+ export interface TodoCompletionTransition {
39
+ phase: string;
40
+ content: string;
41
+ }
42
+
38
43
  export interface TodoWriteToolDetails {
39
44
  phases: TodoPhase[];
40
45
  storage: "session" | "memory";
46
+ completedTasks?: TodoCompletionTransition[];
41
47
  }
42
48
 
43
49
  // =============================================================================
@@ -97,6 +103,31 @@ function clonePhases(phases: TodoPhase[]): TodoPhase[] {
97
103
  return phases.map(phase => ({ name: phase.name, tasks: phase.tasks.map(cloneTask) }));
98
104
  }
99
105
 
106
+ function todoTransitionKey(phase: string, content: string): string {
107
+ return `${phase}\u0000${content}`;
108
+ }
109
+
110
+ function getCompletionTransitions(previous: TodoPhase[], updated: TodoPhase[]): TodoCompletionTransition[] {
111
+ const previousStatuses = new Map<string, TodoStatus>();
112
+ for (const phase of previous) {
113
+ for (const task of phase.tasks) {
114
+ previousStatuses.set(todoTransitionKey(phase.name, task.content), task.status);
115
+ }
116
+ }
117
+
118
+ const transitions: TodoCompletionTransition[] = [];
119
+ for (const phase of updated) {
120
+ for (const task of phase.tasks) {
121
+ if (task.status !== "completed") continue;
122
+ const previousStatus = previousStatuses.get(todoTransitionKey(phase.name, task.content));
123
+ if (previousStatus && previousStatus !== "completed") {
124
+ transitions.push({ phase: phase.name, content: task.content });
125
+ }
126
+ }
127
+ }
128
+ return transitions;
129
+ }
130
+
100
131
  function normalizeInProgressTask(phases: TodoPhase[]): void {
101
132
  const orderedTasks = phases.flatMap(phase => phase.tasks);
102
133
  if (orderedTasks.length === 0) return;
@@ -577,13 +608,16 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
577
608
  _context?: AgentToolContext,
578
609
  ): Promise<AgentToolResult<TodoWriteToolDetails>> {
579
610
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
580
- const { phases: updated, errors } = applyParams(previousPhases, params);
611
+ const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
612
+ const completedTasks = getCompletionTransitions(previousPhases, updated);
581
613
  this.session.setTodoPhases?.(updated);
582
614
  const storage = this.session.getSessionFile() ? "session" : "memory";
615
+ const details: TodoWriteToolDetails = { phases: updated, storage };
616
+ if (completedTasks.length > 0) details.completedTasks = completedTasks;
583
617
 
584
618
  return {
585
619
  content: [{ type: "text", text: formatSummary(updated, errors) }],
586
- details: { phases: updated, storage },
620
+ details,
587
621
  isError: errors.length > 0 ? true : undefined,
588
622
  };
589
623
  }
@@ -667,16 +701,55 @@ function noteMarker(count: number, uiTheme: Theme): string {
667
701
  return uiTheme.fg("dim", chalk.italic(` \u207a${toSuperscript(count)}`));
668
702
  }
669
703
 
670
- function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
704
+ export const TODO_WRITE_STRIKE_HOLD_FRAMES = 2;
705
+ export const TODO_WRITE_STRIKE_REVEAL_FRAMES = 12;
706
+ export const TODO_WRITE_STRIKE_TOTAL_FRAMES = TODO_WRITE_STRIKE_HOLD_FRAMES + TODO_WRITE_STRIKE_REVEAL_FRAMES;
707
+ const EMPTY_COMPLETION_KEYS = new Set<string>();
708
+ const STRIKE_START = "\x1b[9m";
709
+ const STRIKE_END = "\x1b[29m";
710
+
711
+ function strikethroughText(text: string): string {
712
+ return `${STRIKE_START}${text}${STRIKE_END}`;
713
+ }
714
+
715
+ function partialStrikethrough(text: string, visibleChars: number): string {
716
+ if (visibleChars <= 0) return text;
717
+ const chars = [...text];
718
+ if (visibleChars >= chars.length) return strikethroughText(text);
719
+ return `${strikethroughText(chars.slice(0, visibleChars).join(""))}${chars.slice(visibleChars).join("")}`;
720
+ }
721
+
722
+ function strikeRevealCount(text: string, frame: number | undefined): number | undefined {
723
+ if (frame === undefined) return undefined;
724
+ if (frame <= TODO_WRITE_STRIKE_HOLD_FRAMES) return 0;
725
+ const chars = [...text];
726
+ if (chars.length === 0) return undefined;
727
+ const revealFrame = Math.min(frame - TODO_WRITE_STRIKE_HOLD_FRAMES, TODO_WRITE_STRIKE_REVEAL_FRAMES);
728
+ return Math.ceil((chars.length * revealFrame) / TODO_WRITE_STRIKE_REVEAL_FRAMES);
729
+ }
730
+
731
+ function formatTodoLine(
732
+ item: TodoItem,
733
+ uiTheme: Theme,
734
+ prefix: string,
735
+ completionKeys: Set<string>,
736
+ frame: number | undefined,
737
+ ): string {
671
738
  const checkbox = uiTheme.checkbox;
672
739
  const marker = noteMarker(item.notes?.length ?? 0, uiTheme);
673
740
  switch (item.status) {
674
- case "completed":
675
- return uiTheme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(item.content)}`) + marker;
741
+ case "completed": {
742
+ const revealCount = completionKeys.has(item.content) ? strikeRevealCount(item.content, frame) : undefined;
743
+ const content =
744
+ revealCount === undefined
745
+ ? strikethroughText(item.content)
746
+ : partialStrikethrough(item.content, revealCount);
747
+ return uiTheme.fg("success", `${prefix}${checkbox.checked} ${content}`) + marker;
748
+ }
676
749
  case "in_progress":
677
750
  return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`) + marker;
678
751
  case "abandoned":
679
- return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(item.content)}`) + marker;
752
+ return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`) + marker;
680
753
  default:
681
754
  return uiTheme.fg("dim", `${prefix}${checkbox.unchecked} ${item.content}`) + marker;
682
755
  }
@@ -722,6 +795,16 @@ export const todoWriteToolRenderer = {
722
795
  _args?: TodoWriteRenderArgs,
723
796
  ): Component {
724
797
  const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
798
+ const completedTasks = result.details?.completedTasks ?? [];
799
+ const completionKeysByPhase = new Map<string, Set<string>>();
800
+ for (const task of completedTasks) {
801
+ let keys = completionKeysByPhase.get(task.phase);
802
+ if (!keys) {
803
+ keys = new Set<string>();
804
+ completionKeysByPhase.set(task.phase, keys);
805
+ }
806
+ keys.add(task.content);
807
+ }
725
808
  const allTasks = phases.flatMap(phase => phase.tasks);
726
809
  const header = renderStatusLine(
727
810
  { icon: "success", title: "Todo Write", meta: [`${allTasks.length} tasks`] },
@@ -732,29 +815,45 @@ export const todoWriteToolRenderer = {
732
815
  return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
733
816
  }
734
817
 
735
- const { expanded } = options;
736
- const lines: string[] = [header];
737
- for (let p = 0; p < phases.length; p++) {
738
- const phase = phases[p];
739
- if (phases.length > 1) {
740
- lines.push(uiTheme.fg("accent", chalk.bold(` ${formatPhaseDisplayName(phase.name, p + 1)}`)));
741
- }
742
- const treeLines = renderTreeList(
743
- {
744
- items: phase.tasks,
745
- expanded,
746
- maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
747
- itemType: "todo",
748
- renderItem: todo => formatTodoLine(todo, uiTheme, ""),
749
- },
750
- uiTheme,
751
- );
752
- for (const line of treeLines) {
753
- lines.push(` ${line}`);
754
- }
755
- }
756
- lines.push(...renderNoteAttachments(phases, uiTheme));
757
- return new Text(lines.join("\n"), 0, 0);
818
+ let cachedKey: string | undefined;
819
+ let cachedLines: string[] | undefined;
820
+ return {
821
+ invalidate(): void {
822
+ cachedKey = undefined;
823
+ cachedLines = undefined;
824
+ },
825
+ render(width: number): string[] {
826
+ const { expanded, spinnerFrame } = options;
827
+ const key = `${expanded ? 1 : 0}:${spinnerFrame ?? -1}:${width}`;
828
+ if (cachedKey === key && cachedLines) return cachedLines;
829
+
830
+ const lines: string[] = [header];
831
+ for (let p = 0; p < phases.length; p++) {
832
+ const phase = phases[p];
833
+ if (phases.length > 1) {
834
+ lines.push(uiTheme.fg("accent", chalk.bold(` ${formatPhaseDisplayName(phase.name, p + 1)}`)));
835
+ }
836
+ const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
837
+ const treeLines = renderTreeList(
838
+ {
839
+ items: phase.tasks,
840
+ expanded,
841
+ maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
842
+ itemType: "todo",
843
+ renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
844
+ },
845
+ uiTheme,
846
+ );
847
+ for (const line of treeLines) {
848
+ lines.push(` ${line}`);
849
+ }
850
+ }
851
+ lines.push(...renderNoteAttachments(phases, uiTheme));
852
+ cachedKey = key;
853
+ cachedLines = lines;
854
+ return lines;
855
+ },
856
+ };
758
857
  },
759
858
  mergeCallAndResult: true,
760
859
  };
@@ -12,6 +12,7 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
12
12
  #details: TDetails;
13
13
  #meta = outputMeta();
14
14
  #content: ToolContent = [];
15
+ #isError = false;
15
16
 
16
17
  constructor(details?: TDetails) {
17
18
  this.#details = details ?? ({} as TDetails);
@@ -67,6 +68,12 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
67
68
  return this;
68
69
  }
69
70
 
71
+ /** Flag the result as a non-throwing failure (agent-loop surfaces it as a tool error). */
72
+ error(value = true): this {
73
+ this.#isError = value;
74
+ return this;
75
+ }
76
+
70
77
  done(): AgentToolResult<TDetails> {
71
78
  const meta = this.#meta.get();
72
79
  if (meta) {
@@ -77,6 +84,7 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
77
84
  return {
78
85
  content: this.#content,
79
86
  details: hasDetails ? this.#details : undefined,
87
+ ...(this.#isError ? { isError: true } : {}),
80
88
  };
81
89
  }
82
90
  }
@@ -26,6 +26,8 @@ export interface CodeCellOptions {
26
26
  outputMaxLines?: number;
27
27
  codeMaxLines?: number;
28
28
  expanded?: boolean;
29
+ /** Animate the cell border with a sweeping segment while pending/running. */
30
+ animate?: boolean;
29
31
  width: number;
30
32
  }
31
33
 
@@ -130,7 +132,10 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
130
132
  sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
131
133
  }
132
134
 
133
- return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
135
+ return renderOutputBlock(
136
+ { header: title, headerMeta: meta, state, sections, width, animate: options.animate },
137
+ theme,
138
+ );
134
139
  }
135
140
 
136
141
  export interface MarkdownCellOptions {