@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -12,7 +12,7 @@
12
12
  import type { AgentRef } from "../registry/agent-registry";
13
13
  import { AgentRegistry } from "../registry/agent-registry";
14
14
  import { formatSessionHistoryMarkdown } from "../session/session-history-format";
15
- import { loadSessionMessagesReadOnly } from "../session/session-manager";
15
+ import { loadSessionMessagesReadOnly } from "../session/session-loader";
16
16
  import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
17
 
18
18
  /** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
@@ -26,6 +26,20 @@ function toLocalValidationError(error: unknown): Error {
26
26
  const message = error instanceof Error ? error.message : String(error);
27
27
  return new Error(message.replace("skill://", "local://"));
28
28
  }
29
+ const WINDOWS_LOCAL_ROOT_MAX_CHARS = 180;
30
+
31
+ function safeSessionId(options: LocalProtocolOptions): string {
32
+ const raw = options.getSessionId?.() ?? "session";
33
+ const safe = raw.replace(/[^a-zA-Z0-9_.-]/g, "_");
34
+ return safe.length > 0 ? safe : "session";
35
+ }
36
+
37
+ function shortLocalRoot(options: LocalProtocolOptions): string {
38
+ // Derive the short root from the stable session id, never the artifact path,
39
+ // so `SessionManager.moveTo()` and the resume-after-move flow keep finding
40
+ // the same `local://` directory the session wrote pre-move.
41
+ return path.join(os.tmpdir(), "omp-local", safeSessionId(options));
42
+ }
29
43
 
30
44
  function getContentType(filePath: string): InternalResource["contentType"] {
31
45
  const ext = path.extname(filePath).toLowerCase();
@@ -108,20 +122,28 @@ function extractRelativePath(url: InternalUrl): string {
108
122
  return decoded;
109
123
  }
110
124
 
111
- export function resolveLocalRoot(options: LocalProtocolOptions): string {
125
+ /** Resolve the session-scoped local:// root, shortening long Windows artifact paths before writes hit MAX_PATH. */
126
+ export function resolveLocalRoot(options: LocalProtocolOptions, platform: NodeJS.Platform = process.platform): string {
112
127
  const artifactsDir = options.getArtifactsDir?.();
113
128
  if (artifactsDir) {
114
- return path.resolve(artifactsDir, "local");
129
+ const candidate = path.resolve(artifactsDir, "local");
130
+ if (platform === "win32" && candidate.length >= WINDOWS_LOCAL_ROOT_MAX_CHARS) {
131
+ return shortLocalRoot(options);
132
+ }
133
+ return candidate;
115
134
  }
116
135
 
117
- const sessionId = options.getSessionId?.() ?? "session";
118
- const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_");
119
- return path.join(os.tmpdir(), "omp-local", safeSessionId);
136
+ return path.join(os.tmpdir(), "omp-local", safeSessionId(options));
120
137
  }
121
138
 
122
- export function resolveLocalUrlToPath(input: string | InternalUrl, options: LocalProtocolOptions): string {
139
+ /** Resolve a local:// URL to an on-disk path under the active session's local root. */
140
+ export function resolveLocalUrlToPath(
141
+ input: string | InternalUrl,
142
+ options: LocalProtocolOptions,
143
+ platform: NodeJS.Platform = process.platform,
144
+ ): string {
123
145
  const url = typeof input === "string" ? parseLocalUrl(input) : input;
124
- const localRoot = path.resolve(resolveLocalRoot(options));
146
+ const localRoot = path.resolve(resolveLocalRoot(options, platform));
125
147
  const relativePath = extractRelativePath(url);
126
148
 
127
149
  if (!relativePath) {
package/src/main.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  } from "@oh-my-pi/pi-utils";
22
22
  import chalk from "chalk";
23
23
  import { reset as resetCapabilities } from "./capability";
24
- import type { Args } from "./cli/args";
24
+ import { type Args, reportUnrecognizedFlags } from "./cli/args";
25
25
  import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
26
26
  import { processFileArguments } from "./cli/file-processor";
27
27
  import { buildInitialMessage } from "./cli/initial-message";
@@ -64,7 +64,8 @@ import {
64
64
  } from "./sdk";
65
65
  import type { AgentSession } from "./session/agent-session";
66
66
  import type { AuthStorage } from "./session/auth-storage";
67
- import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
67
+ import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
68
+ import { SessionManager } from "./session/session-manager";
68
69
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
69
70
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
70
71
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
@@ -264,7 +265,7 @@ export async function submitInteractiveInput(
264
265
  InteractiveMode,
265
266
  "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError" | "checkShutdownRequested"
266
267
  >,
267
- session: Pick<AgentSession, "prompt" | "promptCustomMessage">,
268
+ session: Pick<AgentSession, "prompt" | "promptCustomMessage" | "isStreaming">,
268
269
  input: SubmittedUserInput,
269
270
  ): Promise<void> {
270
271
  if (input.cancelled) {
@@ -273,22 +274,32 @@ export async function submitInteractiveInput(
273
274
 
274
275
  try {
275
276
  using _keepalive = new EventLoopKeepalive();
277
+ const streamingBehavior = session.isStreaming ? ("followUp" as const) : undefined;
276
278
  // Continue shortcuts submit an already-started synthetic developer prompt with
277
279
  // no optimistic user message.
278
280
  if (!input.started && !mode.markPendingSubmissionStarted(input)) {
279
281
  return;
280
282
  }
281
283
  if (input.customType) {
282
- await session.promptCustomMessage({
284
+ const message = {
283
285
  customType: input.customType,
284
286
  content: input.text,
285
287
  display: input.display ?? false,
286
- attribution: "agent",
287
- });
288
+ attribution: "agent" as const,
289
+ };
290
+ await (streamingBehavior
291
+ ? session.promptCustomMessage(message, { streamingBehavior })
292
+ : session.promptCustomMessage(message));
288
293
  } else if (input.synthetic) {
294
+ // Synthetic continue shortcuts are hidden developer prompts. The streaming
295
+ // queue (#queueUserMessage) only carries user-attributed messages, so we do
296
+ // NOT pass streamingBehavior here: queueing would silently demote the
297
+ // developer directive to a visible user message. A synthetic submit while
298
+ // streaming keeps its prior behavior (rejected as busy) rather than changing
299
+ // its role.
289
300
  await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
290
301
  } else {
291
- await session.prompt(input.text, { images: input.images });
302
+ await session.prompt(input.text, { images: input.images, ...(streamingBehavior && { streamingBehavior }) });
292
303
  }
293
304
  } catch (error: unknown) {
294
305
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -1197,6 +1208,15 @@ export async function runRootCommand(
1197
1208
  },
1198
1209
  };
1199
1210
  const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
1211
+ // Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
1212
+ // know the real extension flag set. Without this check the unrecognized
1213
+ // token gets silently consumed and any following positional leaks as the
1214
+ // initial prompt — kicking off a real LLM session, MCP connection, and
1215
+ // tool calls (issue #2459). Exit code 2 matches the conventional
1216
+ // "command line usage error" convention.
1217
+ if (reportUnrecognizedFlags(initialArgs)) {
1218
+ process.exit(2);
1219
+ }
1200
1220
  const processedFiles =
1201
1221
  initialArgs.fileArgs.length > 0
1202
1222
  ? await logger.time("processFileArguments", () =>
@@ -0,0 +1,21 @@
1
+ export const MCP_CONNECTING_EVENT_CHANNEL = "mcp:connecting";
2
+
3
+ export type McpConnectingEvent = { serverNames: string[] };
4
+
5
+ export function formatMCPConnectingMessage(serverNames: string[]): string {
6
+ return `Connecting to MCP servers: ${serverNames.join(", ")}…`;
7
+ }
8
+
9
+ /**
10
+ * Runtime validator for the cross-module event payload. The event bus is
11
+ * untyped at runtime, so the subscriber verifies the shape before formatting
12
+ * rather than trusting a cast — a malformed emit is ignored instead of throwing.
13
+ */
14
+ export function isMcpConnectingEvent(data: unknown): data is McpConnectingEvent {
15
+ return (
16
+ typeof data === "object" &&
17
+ data !== null &&
18
+ Array.isArray((data as { serverNames?: unknown }).serverNames) &&
19
+ (data as { serverNames: unknown[] }).serverNames.every(name => typeof name === "string")
20
+ );
21
+ }
@@ -589,7 +589,8 @@ export class StdioTransport implements MCPTransport {
589
589
  }
590
590
 
591
591
  if (this.#readLoop) {
592
- await this.#readLoop.catch(() => {});
592
+ // Do not block/await the read loop as it can hang indefinitely in some environments
593
+ this.#readLoop.catch(() => {});
593
594
  this.#readLoop = null;
594
595
  }
595
596
  }
@@ -5,11 +5,12 @@ import * as path from "node:path";
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import { type ApiKey, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
7
7
  import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
8
- import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
+ import { getAgentDbPath, getMemoriesDir, isEnoent, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
9
9
 
10
10
  import type { ModelRegistry } from "../config/model-registry";
11
11
  import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
12
12
  import type { Settings } from "../config/settings";
13
+ import type { MemoryBackendSaveInput, MemoryBackendSaveResult } from "../memory-backend/types";
13
14
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
14
15
  import consolidationSystemTemplate from "../prompts/memories/consolidation_system.md" with { type: "text" };
15
16
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -156,22 +157,31 @@ export async function buildMemoryToolDeveloperInstructions(
156
157
  const cfg = loadMemoryConfig(settings);
157
158
  if (!cfg.enabled) return undefined;
158
159
  const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
159
- const summaryPath = path.join(memoryRoot, "memory_summary.md");
160
160
 
161
- let text: string;
161
+ let summary = "";
162
162
  try {
163
- text = await Bun.file(summaryPath).text();
163
+ summary = (await Bun.file(path.join(memoryRoot, "memory_summary.md")).text()).trim();
164
164
  } catch {
165
- return undefined;
165
+ // Missing or unreadable summary — injection is best-effort; fall through
166
+ // so any captured lessons still surface on their own.
166
167
  }
167
-
168
- const summary = text.trim();
169
- if (!summary) return undefined;
170
- const truncated = truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit);
171
- if (!truncated.trim()) return undefined;
168
+ const learned = await readLearnedLessons(memoryRoot);
169
+ if (!summary && !learned) return undefined;
170
+
171
+ const summaryOut = summary ? truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit).trim() : "";
172
+ // Lessons share ONE injection budget with the summary so the combined block
173
+ // stays within `summaryInjectionTokenLimit` (~4 chars/token, matching
174
+ // truncateByApproxTokens). With no summary, lessons get the whole budget.
175
+ // Clamp to 0: truncateByApproxTokens appends a marker, so a truncated summary
176
+ // can exceed `limit * 4` chars and drive the remainder negative — when the
177
+ // summary already fills the budget, lessons are simply dropped.
178
+ const learnedBudget = Math.max(0, cfg.summaryInjectionTokenLimit - Math.ceil(summaryOut.length / 4));
179
+ const learnedOut = learned && learnedBudget > 0 ? truncateByApproxTokens(learned, learnedBudget).trim() : "";
180
+ if (!summaryOut && !learnedOut) return undefined;
172
181
 
173
182
  return prompt.render(readPathTemplate, {
174
- memory_summary: truncated,
183
+ memory_summary: summaryOut,
184
+ learned: learnedOut,
175
185
  });
176
186
  }
177
187
 
@@ -982,6 +992,12 @@ function redactSecrets(input: string): string {
982
992
  /(?:sk|pk|rk|tok|key|secret|token|password)[-_A-Za-z0-9]{12,}/g,
983
993
  /[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
984
994
  /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
995
+ // Common provider token prefixes (GitHub, npm, Slack, Google).
996
+ /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
997
+ /github_pat_[A-Za-z0-9_]{20,}/g,
998
+ /npm_[A-Za-z0-9]{30,}/g,
999
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
1000
+ /AIza[A-Za-z0-9_-]{30,}/g,
985
1001
  ];
986
1002
  for (const pattern of patterns) {
987
1003
  out = out.replace(pattern, "[REDACTED]");
@@ -1121,6 +1137,125 @@ export function getMemoryRoot(agentDir: string, cwd: string): string {
1121
1137
  return path.join(getMemoriesDir(agentDir), encodeProjectPath(cwd));
1122
1138
  }
1123
1139
 
1140
+ /**
1141
+ * Filename of the captured-lessons file under a project's memory root.
1142
+ *
1143
+ * Written by the `learn` tool via {@link saveLearnedLesson} and read back by
1144
+ * {@link buildMemoryToolDeveloperInstructions}. Deliberately distinct from the
1145
+ * consolidation artifacts (`MEMORY.md`, `memory_summary.md`, `skills/`) so a
1146
+ * consolidation pass never clobbers manually captured lessons.
1147
+ */
1148
+ const LEARNED_LESSONS_FILE = "learned.md";
1149
+ /** Newest-first cap on retained lessons, bounding file growth by entry count. */
1150
+ const MAX_LEARNED_LESSONS = 100;
1151
+ /** Per-field char caps so a single huge capture can't bloat learned.md. */
1152
+ const MAX_LEARNED_CONTENT_CHARS = 2000;
1153
+ const MAX_LEARNED_CONTEXT_CHARS = 400;
1154
+
1155
+ /**
1156
+ * Strip prompt-injection vectors from a single line of lesson text: control/
1157
+ * format chars, angle brackets (`</skills>`), backticks, and `~~~` fences, then
1158
+ * collapse whitespace. Applied on BOTH write and read (the block renders
1159
+ * unescaped into the system prompt), mirroring managed-skill descriptions.
1160
+ */
1161
+ function neutralizeInjection(text: string): string {
1162
+ return text
1163
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ")
1164
+ .replace(/[<>`]/g, "")
1165
+ .replace(/~{2,}/g, "~")
1166
+ .replace(/\s+/g, " ")
1167
+ .trim();
1168
+ }
1169
+
1170
+ /** Slice to `maxChars`, dropping a trailing unpaired high surrogate. */
1171
+ function boundChars(text: string, maxChars: number): string {
1172
+ if (text.length <= maxChars) return text;
1173
+ const sliced = text.slice(0, maxChars);
1174
+ return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
1175
+ }
1176
+
1177
+ /**
1178
+ * Normalize one lesson field for storage: neutralize injection delimiters
1179
+ * FIRST, then redact secrets (so delimiter stripping can't reassemble a token
1180
+ * the redactor would have caught), then bound the length.
1181
+ */
1182
+ function normalizeLearnedText(text: string, maxChars: number): string {
1183
+ return boundChars(redactSecrets(neutralizeInjection(text)).trim(), maxChars);
1184
+ }
1185
+
1186
+ /** Per-path write chains serializing `learned.md` read-modify-write. */
1187
+ const learnedWriteChains = new Map<string, Promise<unknown>>();
1188
+
1189
+ /**
1190
+ * Append one lesson to the project's `learned.md` (newest-first, deduped,
1191
+ * capped, secret-redacted, injection-neutralized). The file backs the `learn`
1192
+ * tool when `memory.backend` is `local`.
1193
+ */
1194
+ export async function saveLearnedLesson(
1195
+ agentDir: string,
1196
+ cwd: string,
1197
+ input: MemoryBackendSaveInput,
1198
+ ): Promise<MemoryBackendSaveResult> {
1199
+ const content = normalizeLearnedText(input.content, MAX_LEARNED_CONTENT_CHARS);
1200
+ if (!content) {
1201
+ return { backend: "local", stored: 0, message: "Empty lesson; nothing stored." };
1202
+ }
1203
+ const context = input.context ? normalizeLearnedText(input.context, MAX_LEARNED_CONTEXT_CHARS) : "";
1204
+ const line = context ? `- ${content} _(context: ${context})_` : `- ${content}`;
1205
+ const filePath = path.join(getMemoryRoot(agentDir, cwd), LEARNED_LESSONS_FILE);
1206
+
1207
+ // Serialize the read-modify-write per file: parallel `learn` calls (sibling
1208
+ // subagents, or two shared tool calls in one turn) share the project memory
1209
+ // root, so an unguarded RMW would let the last writer drop the other's lesson.
1210
+ const run = (learnedWriteChains.get(filePath) ?? Promise.resolve()).then(() => appendLearnedLine(filePath, line));
1211
+ const guarded = run.catch(() => {});
1212
+ learnedWriteChains.set(filePath, guarded);
1213
+ try {
1214
+ await run;
1215
+ } finally {
1216
+ // Drop the entry once this write is the chain tail, so the map does not
1217
+ // retain one promise per distinct memory root for the process lifetime.
1218
+ if (learnedWriteChains.get(filePath) === guarded) learnedWriteChains.delete(filePath);
1219
+ }
1220
+ return { backend: "local", stored: 1, message: `Lesson saved to ${LEARNED_LESSONS_FILE}.` };
1221
+ }
1222
+
1223
+ async function appendLearnedLine(filePath: string, line: string): Promise<void> {
1224
+ let existing = "";
1225
+ try {
1226
+ existing = await Bun.file(filePath).text();
1227
+ } catch (err) {
1228
+ if (!isEnoent(err)) throw err;
1229
+ }
1230
+ const prior = existing
1231
+ .split("\n")
1232
+ .map(l => l.trim())
1233
+ .filter(l => l.startsWith("- ") && l !== line);
1234
+ const lessons = [line, ...prior].slice(0, MAX_LEARNED_LESSONS);
1235
+ await Bun.write(filePath, `${lessons.join("\n")}\n`);
1236
+ }
1237
+
1238
+ /**
1239
+ * Read `learned.md`, neutralizing each line on read too — a hand-edited or
1240
+ * pre-existing file bypasses write-time normalization and the block renders
1241
+ * unescaped into the system prompt. Returns "" when absent/unreadable.
1242
+ */
1243
+ async function readLearnedLessons(memoryRoot: string): Promise<string> {
1244
+ let raw = "";
1245
+ try {
1246
+ raw = (await Bun.file(path.join(memoryRoot, LEARNED_LESSONS_FILE)).text()).trim();
1247
+ } catch {
1248
+ return "";
1249
+ }
1250
+ if (!raw) return "";
1251
+ // Neutralize delimiters THEN redact per line — mirrors the write path so a
1252
+ // hand-edited line cannot reassemble a token after delimiter stripping.
1253
+ return raw
1254
+ .split("\n")
1255
+ .map(line => redactSecrets(neutralizeInjection(line)))
1256
+ .join("\n");
1257
+ }
1258
+
1124
1259
  function encodeProjectPath(cwd: string): string {
1125
1260
  return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1126
1261
  }
@@ -2,6 +2,7 @@ import {
2
2
  buildMemoryToolDeveloperInstructions,
3
3
  clearMemoryData,
4
4
  enqueueMemoryConsolidation,
5
+ saveLearnedLesson,
5
6
  startMemoryStartupTask,
6
7
  } from "../memories";
7
8
  import type { MemoryBackend } from "./types";
@@ -9,9 +10,10 @@ import type { MemoryBackend } from "./types";
9
10
  /**
10
11
  * Wraps the existing `memories/` module as a `MemoryBackend`.
11
12
  *
12
- * No behavioural change every call delegates to the legacy entry points so
13
- * the local memory pipeline (rollout summarisation SQLite → memory_summary.md)
14
- * keeps working exactly as before.
13
+ * The rollout-summarisation pipeline (rollouts SQLite memory_summary.md) is
14
+ * delegated unchanged. On top of it, `save()` persists `learn`-tool lessons to
15
+ * `learned.md` (so `status()` reports `writable: true`); structured search is
16
+ * still unavailable.
15
17
  */
16
18
  export const localBackend: MemoryBackend = {
17
19
  id: "local",
@@ -27,13 +29,17 @@ export const localBackend: MemoryBackend = {
27
29
  async enqueue(agentDir, cwd) {
28
30
  enqueueMemoryConsolidation(agentDir, cwd);
29
31
  },
32
+ async save(context, input) {
33
+ return saveLearnedLesson(context.agentDir, context.cwd, input);
34
+ },
30
35
  async status() {
31
36
  return {
32
37
  backend: "local" as const,
33
38
  active: true,
34
- writable: false,
39
+ writable: true,
35
40
  searchable: false,
36
- message: "Local rollout-summary memory is active; structured search/save is not available.",
41
+ message:
42
+ "Local rollout-summary memory is active; lessons from the `learn` tool are saved to learned.md. Structured search is not available.",
37
43
  };
38
44
  },
39
45
  };
@@ -305,6 +305,7 @@ function createStatsMemory(config: MnemopiBackendConfig, bank: string): Mnemopi
305
305
  authorType: "agent",
306
306
  channelId: bank,
307
307
  ...providerOptions,
308
+ reconcile: false,
308
309
  } as ConstructorParameters<typeof Mnemopi>[0]);
309
310
  }
310
311
 
@@ -48,6 +48,17 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
48
48
  const recallBanks =
49
49
  scoping === "global" ? scope.recallBanks : extendRecallWithLegacyBanks(scope.recallBanks, dbPath, cwd);
50
50
  const llmMode = settings.get("mnemopi.llmMode");
51
+ const embeddingOverride = settings.get("mnemopi.embeddingModel");
52
+ const embeddingVariant = settings.get("mnemopi.embeddingVariant");
53
+ // Map the variant explicitly rather than indexing an object with the raw config
54
+ // value (which could resolve an inherited property like `__proto__`); any value
55
+ // other than the multilingual variant falls back to the English default.
56
+ const variantModel =
57
+ embeddingVariant === "multilingual" ? "intfloat/multilingual-e5-large" : "BAAI/bge-base-en-v1.5";
58
+ // Precedence: explicit `mnemopi.embeddingModel` setting > `MNEMOPI_EMBEDDING_MODEL`
59
+ // env (documented model-level override) > variant-derived default. Without the env
60
+ // term a variant default would silently shadow a user's configured env model.
61
+ const embeddingModel = embeddingOverride?.trim() || Bun.env.MNEMOPI_EMBEDDING_MODEL?.trim() || variantModel;
51
62
  return {
52
63
  dbPath,
53
64
  baseBank: scope.baseBank,
@@ -69,7 +80,7 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
69
80
  providerOptions: {
70
81
  noEmbeddings: settings.get("mnemopi.noEmbeddings"),
71
82
  debug: settings.get("mnemopi.debug"),
72
- embeddingModel: settings.get("mnemopi.embeddingModel"),
83
+ embeddingModel,
73
84
  embeddingApiUrl: settings.get("mnemopi.embeddingApiUrl"),
74
85
  embeddingApiKey: settings.get("mnemopi.embeddingApiKey"),
75
86
  llm:
@@ -172,10 +183,10 @@ function projectBankSegment(projectRoot: string): string {
172
183
 
173
184
  /**
174
185
  * Discover sibling banks under `<dbDir>/banks/` whose `working_memory` rows
175
- * already carry the active `cwd` in `metadata_json.$.cwd`, and add them to
176
- * the recall set. This rescues memories stranded by a previous, less-stable
177
- * bank derivation (#2412) without changing the write target — only recall is
178
- * widened.
186
+ * all carry the active `cwd` in `metadata_json.$.cwd`, and add those safe
187
+ * single-cwd banks to the recall set. This rescues memories stranded by a
188
+ * previous, less-stable bank derivation (#2412) without recalling mixed-cwd
189
+ * legacy banks wholesale under per-project isolation.
179
190
  *
180
191
  * Robust by design: a missing banks directory, unreadable bank dir, or
181
192
  * corrupt SQLite file is silently skipped. Scanning is capped at
@@ -202,19 +213,24 @@ export function extendRecallWithLegacyBanks(
202
213
  if (scanned >= LEGACY_BANK_SCAN_LIMIT) break;
203
214
  scanned++;
204
215
  const candidate = path.join(banksDir, entry.name, "mnemopi.db");
205
- if (bankHasCwd(candidate, cwdAbs)) extras.push(entry.name);
216
+ if (bankOnlyHasCwd(candidate, cwdAbs)) extras.push(entry.name);
206
217
  }
207
218
  return extras.length === 0 ? resolved : [...resolved, ...extras];
208
219
  }
209
220
 
210
- function bankHasCwd(dbPath: string, cwd: string): boolean {
221
+ function bankOnlyHasCwd(dbPath: string, cwd: string): boolean {
211
222
  let db: Database | undefined;
212
223
  try {
213
224
  db = new Database(dbPath, { readonly: true });
214
225
  const row = db
215
- .query("SELECT 1 FROM working_memory WHERE json_extract(metadata_json, '$.cwd') = ? LIMIT 1")
216
- .get(cwd);
217
- return row !== null;
226
+ .prepare<{ matching: number; unsafe: number }, [string, string]>(`
227
+ SELECT
228
+ SUM(CASE WHEN json_extract(metadata_json, '$.cwd') = ? THEN 1 ELSE 0 END) AS matching,
229
+ SUM(CASE WHEN json_extract(metadata_json, '$.cwd') IS NULL OR json_extract(metadata_json, '$.cwd') <> ? THEN 1 ELSE 0 END) AS unsafe
230
+ FROM working_memory
231
+ `)
232
+ .get(cwd, cwd);
233
+ return (row?.matching ?? 0) > 0 && (row?.unsafe ?? 0) === 0;
218
234
  } catch (error) {
219
235
  logger.debug("Mnemopi: legacy bank probe failed", { dbPath, error: String(error) });
220
236
  return false;
@@ -63,11 +63,9 @@ import { theme } from "../../modes/theme/theme";
63
63
  import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
64
64
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
65
65
  import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
66
- import {
67
- SessionManager,
68
- type SessionInfo as StoredSessionInfo,
69
- type UsageStatistics,
70
- } from "../../session/session-manager";
66
+ import type { UsageStatistics } from "../../session/session-entries";
67
+ import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
68
+ import { SessionManager } from "../../session/session-manager";
71
69
  import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
72
70
  import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
73
71
  import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
@@ -16,6 +16,7 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
19
+ import type { Usage } from "@oh-my-pi/pi-ai";
19
20
  import { Container, Editor, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
20
21
  import { formatAge, formatBytes, formatDuration, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
21
22
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
@@ -35,8 +36,8 @@ import {
35
36
  type SkillPromptDetails,
36
37
  USER_INTERRUPT_LABEL,
37
38
  } from "../../session/messages";
38
- import type { SessionMessageEntry } from "../../session/session-manager";
39
- import { parseSessionEntries } from "../../session/session-manager";
39
+ import type { SessionMessageEntry } from "../../session/session-entries";
40
+ import { parseSessionEntries } from "../../session/session-loader";
40
41
  import { createIrcMessageCard } from "../../tools/irc";
41
42
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
42
43
  import { hasVisibleThinking } from "../../utils/thinking-display";
@@ -47,7 +48,7 @@ import { AssistantMessageComponent } from "./assistant-message";
47
48
  import { BashExecutionComponent } from "./bash-execution";
48
49
  import { BranchSummaryMessageComponent } from "./branch-summary-message";
49
50
  import { CollabPromptMessageComponent } from "./collab-prompt-message";
50
- import { CompactionSummaryMessageComponent } from "./compaction-summary-message";
51
+ import { CompactionSummaryMessageComponent, createHandoffSummaryMessageComponent } from "./compaction-summary-message";
51
52
  import { CustomMessageComponent } from "./custom-message";
52
53
  import { DynamicBorder } from "./dynamic-border";
53
54
  import { EvalExecutionComponent } from "./eval-execution";
@@ -57,6 +58,7 @@ import { SkillMessageComponent } from "./skill-message";
57
58
  import { formatContextUsage } from "./status-line/context-thresholds";
58
59
  import { ToolExecutionComponent } from "./tool-execution";
59
60
  import { TranscriptBlock, TranscriptContainer } from "./transcript-container";
61
+ import { createUsageRowBlock } from "./usage-row";
60
62
  import { UserMessageComponent } from "./user-message";
61
63
 
62
64
  /** Lines per page for PageUp/PageDown */
@@ -213,6 +215,7 @@ export class AgentHubOverlayComponent extends Container {
213
215
  #chatPendingTools = new Map<string, ToolExecutionComponent | ReadToolGroupComponent>();
214
216
  #chatReadArgs = new Map<string, Record<string, unknown>>();
215
217
  #chatReadGroup: ReadToolGroupComponent | null = null;
218
+ #pendingUsage: Usage | undefined;
216
219
  #chatWaitingPoll: ToolExecutionComponent | null = null;
217
220
  #chatExpandables: Array<{ setExpanded(expanded: boolean): void }> = [];
218
221
  #chatExpanded = false;
@@ -264,6 +267,15 @@ export class AgentHubOverlayComponent extends Container {
264
267
  this.#refreshRows();
265
268
  }
266
269
 
270
+ /**
271
+ * Whether the table view has no agents to show (every registered agent except
272
+ * Main, after the persisted-subagent scan in the constructor). The double-←
273
+ * gesture reads this to stay inert when there is nothing to open.
274
+ */
275
+ get isEmpty(): boolean {
276
+ return this.#rows.length === 0;
277
+ }
278
+
267
279
  /** Tear down every subscription and timer. Called by the overlay owner on close. */
268
280
  dispose(): void {
269
281
  for (const unsubscribe of this.#unsubscribers.splice(0)) unsubscribe();
@@ -438,6 +450,7 @@ export class AgentHubOverlayComponent extends Container {
438
450
  #renderRow(ref: AgentRef, selected: boolean, width: number): string {
439
451
  const cursor = selected ? theme.fg("accent", theme.nav.cursor) : " ";
440
452
  const parts: string[] = [statusBadge(ref.status), theme.bold(replaceTabs(ref.id))];
453
+ parts.push(theme.fg("dim", replaceTabs(ref.displayName)));
441
454
  parts.push(theme.fg("dim", ref.parentId ? `${ref.kind} · of ${ref.parentId}` : ref.kind));
442
455
  const observed = this.#observableFor(ref.id);
443
456
  const task = observed?.description ?? observed?.progress?.task;
@@ -850,6 +863,7 @@ export class AgentHubOverlayComponent extends Container {
850
863
  this.#chatPendingTools.clear();
851
864
  this.#chatReadArgs.clear();
852
865
  this.#chatReadGroup = null;
866
+ this.#pendingUsage = undefined;
853
867
  this.#chatWaitingPoll = null;
854
868
  this.#chatExpandables = [];
855
869
  this.#chatLog.dispose();
@@ -869,6 +883,13 @@ export class AgentHubOverlayComponent extends Container {
869
883
  this.#appendChatMessage(entries[i].message);
870
884
  }
871
885
  this.#chatBuiltCount = entries.length;
886
+ // Flush the trailing turn's usage row only once its tools are materialized.
887
+ // A read (or any tool) whose toolResult lands in a later debounced sync stays
888
+ // pending in #chatReadArgs / #chatPendingTools; flushing now would emit the
889
+ // row above it. The sync that drains the maps flushes it below the tools.
890
+ if (this.#chatReadArgs.size === 0 && this.#chatPendingTools.size === 0) {
891
+ this.#flushPendingUsage();
892
+ }
872
893
  }
873
894
 
874
895
  #trackExpandable(component: { setExpanded(expanded: boolean): void }): void {
@@ -898,7 +919,21 @@ export class AgentHubOverlayComponent extends Container {
898
919
  return this.#chatReadGroup;
899
920
  }
900
921
 
922
+ // The per-turn token-usage row must land below the turn's tool blocks, but
923
+ // normal `read` calls only materialize their group in #appendToolResult. Defer
924
+ // the row: stash it on the assistant message and flush once the turn's tools
925
+ // are placed — before the next non-toolResult message and at the end of each
926
+ // sync pass — sealing the read run so the row sits under it.
927
+ #flushPendingUsage(): void {
928
+ if (!this.#pendingUsage) return;
929
+ this.#chatReadGroup?.seal();
930
+ this.#chatReadGroup = null;
931
+ this.#chatLog.addChild(createUsageRowBlock(this.#pendingUsage));
932
+ this.#pendingUsage = undefined;
933
+ }
934
+
901
935
  #appendChatMessage(message: AgentMessage): void {
936
+ if (message.role !== "toolResult") this.#flushPendingUsage();
902
937
  switch (message.role) {
903
938
  case "assistant":
904
939
  this.#appendAssistantMessage(message);
@@ -987,7 +1022,6 @@ export class AgentHubOverlayComponent extends Container {
987
1022
  const assistantComponent = new AssistantMessageComponent(message, this.#hideThinkingBlock?.() ?? false, () =>
988
1023
  this.#requestRender(),
989
1024
  );
990
- assistantComponent.setUsageInfo(message.usage);
991
1025
  this.#chatLog.addChild(assistantComponent);
992
1026
 
993
1027
  const hasVisibleAssistantContent = message.content.some(
@@ -1066,6 +1100,8 @@ export class AgentHubOverlayComponent extends Container {
1066
1100
  this.#chatPendingTools.set(content.id, component);
1067
1101
  }
1068
1102
  }
1103
+
1104
+ this.#pendingUsage = settings.get("display.showTokenUsage") ? message.usage : undefined;
1069
1105
  }
1070
1106
 
1071
1107
  #appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
@@ -1179,6 +1215,15 @@ export class AgentHubOverlayComponent extends Container {
1179
1215
  this.#chatLog.addChild(card);
1180
1216
  return;
1181
1217
  }
1218
+ const handoffComponent = createHandoffSummaryMessageComponent(
1219
+ message as CustomMessage<unknown>,
1220
+ this.#chatExpanded,
1221
+ );
1222
+ if (handoffComponent) {
1223
+ this.#trackExpandable(handoffComponent);
1224
+ this.#chatLog.addChild(handoffComponent);
1225
+ return;
1226
+ }
1182
1227
  const component = new CustomMessageComponent(
1183
1228
  message as CustomMessage<unknown>,
1184
1229
  this.#getMessageRenderer?.(message.customType),