@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
@@ -80,8 +80,12 @@ export type RpcSessionChangeResult =
80
80
  export type RpcSessionChangeSession = Pick<AgentSession, "newSession" | "switchSession" | "branch">;
81
81
 
82
82
  export type RpcSkillCommandSession = Pick<AgentSession, "promptCustomMessage" | "skills" | "skillsSettings">;
83
+ export type RpcSkillCommandResult = { agentInvoked: true };
83
84
 
84
- export async function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string): Promise<boolean> {
85
+ export async function tryRunRpcSkillCommand(
86
+ session: RpcSkillCommandSession,
87
+ text: string,
88
+ ): Promise<RpcSkillCommandResult | false> {
85
89
  if (!text.startsWith("/skill:")) return false;
86
90
  if (!session.skillsSettings?.enableSkillCommands) return false;
87
91
  const spaceIndex = text.indexOf(" ");
@@ -98,8 +102,120 @@ export async function tryRunRpcSkillCommand(session: RpcSkillCommandSession, tex
98
102
  details: built.details,
99
103
  attribution: "user",
100
104
  });
101
- return true;
105
+ return { agentInvoked: true };
102
106
  }
107
+
108
+ export function reportLocalOnlyPromptResult(input: {
109
+ id: string | undefined;
110
+ prompt: Promise<boolean>;
111
+ output: (obj: object) => void;
112
+ onError: (error: Error) => void;
113
+ hasExtensionAgentMessageTask?: () => boolean;
114
+ waitForExtensionAgentMessageTasks?: () => Promise<void>;
115
+ }): void {
116
+ void input.prompt
117
+ .then(async agentInvoked => {
118
+ if (agentInvoked) return;
119
+ await input.waitForExtensionAgentMessageTasks?.();
120
+ if (!input.hasExtensionAgentMessageTask?.()) {
121
+ input.output({ type: "prompt_result", id: input.id, agentInvoked: false });
122
+ }
123
+ })
124
+ .catch(error => {
125
+ input.onError(error instanceof Error ? error : new Error(String(error)));
126
+ });
127
+ }
128
+
129
+ type RpcExtensionUserMessageScope = {
130
+ hasAgentMessageTask: boolean;
131
+ pendingAgentMessageTasks: Set<Promise<void>>;
132
+ };
133
+
134
+ /**
135
+ * Tracks extension-originated messages while an RPC prompt is executing.
136
+ * A slash command can resolve the outer prompt as local-only while also
137
+ * scheduling agent work through pi.sendUserMessage() or pi.sendMessage()
138
+ * with triggerTurn; that prompt must not report agentInvoked:false to the host.
139
+ */
140
+ export class RpcExtensionUserMessageTracker {
141
+ #activePromptScopes = new Set<RpcExtensionUserMessageScope>();
142
+
143
+ markAgentMessageTask(): void {
144
+ for (const scope of this.#activePromptScopes) {
145
+ scope.hasAgentMessageTask = true;
146
+ }
147
+ }
148
+
149
+ trackAgentMessageTask(task: Promise<unknown>): void {
150
+ for (const scope of this.#activePromptScopes) {
151
+ this.#trackAgentMessageTaskForScope(scope, task);
152
+ }
153
+ }
154
+
155
+ #trackAgentMessageTaskForScope(scope: RpcExtensionUserMessageScope, task: Promise<unknown>): void {
156
+ const scopedTask = task.then(
157
+ () => {
158
+ scope.hasAgentMessageTask = true;
159
+ },
160
+ () => {},
161
+ );
162
+ scope.pendingAgentMessageTasks.add(scopedTask);
163
+ void scopedTask.finally(() => {
164
+ scope.pendingAgentMessageTasks.delete(scopedTask);
165
+ });
166
+ }
167
+
168
+ async #waitForAgentMessageTasks(scope: RpcExtensionUserMessageScope): Promise<void> {
169
+ while (scope.pendingAgentMessageTasks.size > 0) {
170
+ await Promise.allSettled(Array.from(scope.pendingAgentMessageTasks));
171
+ }
172
+ }
173
+
174
+ watchPrompt<T>(startPrompt: () => Promise<T>): {
175
+ prompt: Promise<T>;
176
+ hasAgentMessageTask: () => boolean;
177
+ waitForAgentMessageTasks: () => Promise<void>;
178
+ } {
179
+ const scope: RpcExtensionUserMessageScope = {
180
+ hasAgentMessageTask: false,
181
+ pendingAgentMessageTasks: new Set(),
182
+ };
183
+ this.#activePromptScopes.add(scope);
184
+ let prompt: Promise<T>;
185
+ try {
186
+ prompt = startPrompt();
187
+ } catch (error) {
188
+ this.#activePromptScopes.delete(scope);
189
+ throw error;
190
+ }
191
+ return {
192
+ prompt: prompt.finally(() => {
193
+ this.#activePromptScopes.delete(scope);
194
+ }),
195
+ hasAgentMessageTask: () => scope.hasAgentMessageTask,
196
+ waitForAgentMessageTasks: () => this.#waitForAgentMessageTasks(scope),
197
+ };
198
+ }
199
+ }
200
+
201
+ export function watchAndReportLocalOnlyPromptResult(input: {
202
+ id: string | undefined;
203
+ startPrompt: () => Promise<boolean>;
204
+ output: (obj: object) => void;
205
+ onError: (error: Error) => void;
206
+ extensionUserMessageTracker: RpcExtensionUserMessageTracker;
207
+ }): void {
208
+ const trackedPrompt = input.extensionUserMessageTracker.watchPrompt(input.startPrompt);
209
+ reportLocalOnlyPromptResult({
210
+ id: input.id,
211
+ prompt: trackedPrompt.prompt,
212
+ output: input.output,
213
+ onError: input.onError,
214
+ hasExtensionAgentMessageTask: trackedPrompt.hasAgentMessageTask,
215
+ waitForExtensionAgentMessageTasks: trackedPrompt.waitForAgentMessageTasks,
216
+ });
217
+ }
218
+
103
219
  export type RpcSubagentResetRegistry = Pick<RpcSubagentRegistry, "clear">;
104
220
 
105
221
  export async function handleRpcSessionChange(
@@ -277,6 +393,8 @@ export async function runRpcMode(
277
393
  return { id, type: "response", command, success: false, error: message };
278
394
  };
279
395
 
396
+ const extensionUserMessageTracker = new RpcExtensionUserMessageTracker();
397
+
280
398
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
281
399
  const hostToolBridge = new RpcHostToolBridge(output);
282
400
  const hostUriBridge = new RpcHostUriBridge(output);
@@ -533,6 +651,9 @@ export async function runRpcMode(
533
651
  onShutdown: () => {
534
652
  shutdownState.requested = true;
535
653
  },
654
+ trackAgentInvokingMessage: task => {
655
+ extensionUserMessageTracker.trackAgentMessageTask(task);
656
+ },
536
657
  uiContext: rpcUiContext,
537
658
  });
538
659
 
@@ -569,8 +690,9 @@ export async function runRpcMode(
569
690
  // =================================================================
570
691
 
571
692
  case "prompt": {
572
- if (await tryRunRpcSkillCommand(session, command.message)) {
573
- return success(id, "prompt");
693
+ const skillResult = await tryRunRpcSkillCommand(session, command.message);
694
+ if (skillResult) {
695
+ return success(id, "prompt", skillResult);
574
696
  }
575
697
  const builtinResult = await executeAcpBuiltinSlashCommand(command.message, {
576
698
  session,
@@ -589,22 +711,32 @@ export async function runRpcMode(
589
711
  });
590
712
  if (builtinResult !== false) {
591
713
  if ("prompt" in builtinResult) {
592
- session
593
- .prompt(builtinResult.prompt, { images: command.images })
594
- .catch(e => output(error(id, "prompt", e.message)));
714
+ watchAndReportLocalOnlyPromptResult({
715
+ id,
716
+ startPrompt: () => session.prompt(builtinResult.prompt, { images: command.images }),
717
+ output,
718
+ onError: promptError => output(error(id, "prompt", promptError.message)),
719
+ extensionUserMessageTracker,
720
+ });
721
+ return success(id, "prompt");
595
722
  }
596
- return success(id, "prompt");
723
+ return success(id, "prompt", { agentInvoked: false });
597
724
  }
598
725
 
599
726
  // Don't await - events will stream
600
727
  // Extension commands are executed immediately, file prompt templates are expanded
601
728
  // If streaming and streamingBehavior specified, queues via steer/followUp
602
- session
603
- .prompt(command.message, {
604
- images: command.images,
605
- streamingBehavior: command.streamingBehavior,
606
- })
607
- .catch(e => output(error(id, "prompt", e.message)));
729
+ watchAndReportLocalOnlyPromptResult({
730
+ id,
731
+ startPrompt: () =>
732
+ session.prompt(command.message, {
733
+ images: command.images,
734
+ streamingBehavior: command.streamingBehavior,
735
+ }),
736
+ output,
737
+ onError: promptError => output(error(id, "prompt", promptError.message)),
738
+ extensionUserMessageTracker,
739
+ });
608
740
  return success(id, "prompt");
609
741
  }
610
742
 
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { isEnoent } from "@oh-my-pi/pi-utils";
3
- import type { FileEntry, SessionMessageEntry } from "../../session/session-manager";
4
- import { parseSessionEntries } from "../../session/session-manager";
3
+ import type { FileEntry, SessionMessageEntry } from "../../session/session-entries";
4
+ import { parseSessionEntries } from "../../session/session-loader";
5
5
  import {
6
6
  type AgentProgress,
7
7
  type SubagentEventPayload,
@@ -10,7 +10,7 @@ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
10
10
  import type { BashResult } from "../../exec/bash-executor";
11
11
  import type { ContextUsage } from "../../extensibility/extensions/types";
12
12
  import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
13
- import type { FileEntry } from "../../session/session-manager";
13
+ import type { FileEntry } from "../../session/session-entries";
14
14
  import type { AvailableSlashCommandSource } from "../../slash-commands/available-commands";
15
15
  import type {
16
16
  AgentProgress,
@@ -126,6 +126,12 @@ export interface RpcAvailableCommandsUpdateFrame {
126
126
  commands: RpcAvailableSlashCommand[];
127
127
  }
128
128
 
129
+ export interface RpcPromptResultFrame {
130
+ type: "prompt_result";
131
+ id?: string;
132
+ agentInvoked: boolean;
133
+ }
134
+
129
135
  export interface RpcHandoffResult {
130
136
  savedPath?: string;
131
137
  }
@@ -163,7 +169,7 @@ export interface RpcSubagentMessagesResult {
163
169
  // Success responses with data
164
170
  export type RpcResponse =
165
171
  // Prompting (async - events follow)
166
- | { id?: string; type: "response"; command: "prompt"; success: true }
172
+ | { id?: string; type: "response"; command: "prompt"; success: true; data?: { agentInvoked: boolean } }
167
173
  | { id?: string; type: "response"; command: "steer"; success: true }
168
174
  | { id?: string; type: "response"; command: "follow_up"; success: true }
169
175
  | { id?: string; type: "response"; command: "abort"; success: true }
@@ -23,6 +23,10 @@ export interface InitializeExtensionsOptions {
23
23
  onShutdown?: () => void;
24
24
  /** Optional UI context (rpc supplies one; print runs headless). */
25
25
  uiContext?: ExtensionUIContext;
26
+ /** Optional lifecycle hook for extension-originated messages that can start an agent turn. */
27
+ markAgentInvokingMessage?: () => void;
28
+ /** Optional lifecycle hook for extension-originated sends whose success/failure determines turn ownership. */
29
+ trackAgentInvokingMessage?: (task: Promise<unknown>) => void;
26
30
  }
27
31
 
28
32
  /**
@@ -35,19 +39,40 @@ export async function initializeExtensions(session: AgentSession, options: Initi
35
39
  const runner = session.extensionRunner;
36
40
  if (!runner) return;
37
41
 
38
- const { reportSendError, reportRuntimeError, onShutdown, uiContext } = options;
42
+ const {
43
+ reportSendError,
44
+ reportRuntimeError,
45
+ onShutdown,
46
+ uiContext,
47
+ markAgentInvokingMessage,
48
+ trackAgentInvokingMessage,
49
+ } = options;
39
50
  const shutdown = onShutdown ?? (() => {});
40
51
 
41
52
  runner.initialize(
42
53
  // ExtensionActions
43
54
  {
44
55
  sendMessage: (message, sendOptions) => {
45
- session.sendCustomMessage(message, sendOptions).catch(e => {
56
+ const sendTask = session.sendCustomMessage(message, sendOptions);
57
+ if (sendOptions?.triggerTurn) {
58
+ if (trackAgentInvokingMessage) {
59
+ trackAgentInvokingMessage(sendTask);
60
+ } else {
61
+ markAgentInvokingMessage?.();
62
+ }
63
+ }
64
+ sendTask.catch(e => {
46
65
  reportSendError("extension_send", e instanceof Error ? e : new Error(String(e)));
47
66
  });
48
67
  },
49
68
  sendUserMessage: (content, sendOptions) => {
50
- session.sendUserMessage(content, sendOptions).catch(e => {
69
+ const sendTask = session.sendUserMessage(content, sendOptions);
70
+ if (trackAgentInvokingMessage) {
71
+ trackAgentInvokingMessage(sendTask);
72
+ } else {
73
+ markAgentInvokingMessage?.();
74
+ }
75
+ sendTask.catch(e => {
51
76
  reportSendError("extension_send_user", e instanceof Error ? e : new Error(String(e)));
52
77
  });
53
78
  },
@@ -2098,6 +2098,11 @@ var currentThemeName: string | undefined;
2098
2098
  export function getCurrentThemeName(): string | undefined {
2099
2099
  return currentThemeName;
2100
2100
  }
2101
+
2102
+ /** Returns unstyled `text` before `initTheme()` assigns the global theme; use only for early-render paths. */
2103
+ export function fgOrPlain(color: ThemeColor, text: string, styledText: string = text): string {
2104
+ return typeof theme === "undefined" ? text : theme.fg(color, styledText);
2105
+ }
2101
2106
  var currentSymbolPresetOverride: SymbolPreset | undefined;
2102
2107
  var currentColorBlindMode: boolean = false;
2103
2108
  var themeWatcher: fs.FSWatcher | undefined;
@@ -2108,6 +2113,7 @@ var autoDarkTheme: string = "dark";
2108
2113
  var autoLightTheme: string = "light";
2109
2114
  var onThemeChangeCallback: (() => void) | undefined;
2110
2115
  var themeLoadRequestId: number = 0;
2116
+ let themeEpoch = 0;
2111
2117
 
2112
2118
  function getCurrentThemeOptions(): CreateThemeOptions {
2113
2119
  return {
@@ -2160,9 +2166,7 @@ export async function setTheme(
2160
2166
  if (enableWatcher) {
2161
2167
  await startThemeWatcher();
2162
2168
  }
2163
- if (onThemeChangeCallback) {
2164
- onThemeChangeCallback();
2165
- }
2169
+ notifyThemeChange();
2166
2170
  return { success: true };
2167
2171
  } catch (error) {
2168
2172
  if (requestId !== themeLoadRequestId) {
@@ -2171,6 +2175,10 @@ export async function setTheme(
2171
2175
  // Theme is invalid - fall back to dark theme
2172
2176
  currentThemeName = "dark";
2173
2177
  theme = await loadTheme("dark", getCurrentThemeOptions());
2178
+ // The active theme just changed to the fallback — bump the epoch so memoized
2179
+ // renderers (e.g. ToolExecutionComponent) re-shape with the fallback colors
2180
+ // instead of holding the failed theme's stale styling.
2181
+ notifyThemeChange();
2174
2182
  // Don't start watcher for fallback theme
2175
2183
  return {
2176
2184
  success: false,
@@ -2187,9 +2195,7 @@ export async function previewTheme(name: string): Promise<{ success: boolean; er
2187
2195
  return { success: false, error: "Theme preview superseded by a newer request" };
2188
2196
  }
2189
2197
  theme = loadedTheme;
2190
- if (onThemeChangeCallback) {
2191
- onThemeChangeCallback();
2192
- }
2198
+ notifyThemeChange();
2193
2199
  return { success: true };
2194
2200
  } catch (error) {
2195
2201
  if (requestId !== themeLoadRequestId) {
@@ -2236,9 +2242,7 @@ export function setThemeInstance(themeInstance: Theme): void {
2236
2242
  theme = themeInstance;
2237
2243
  currentThemeName = "<in-memory>";
2238
2244
  stopThemeWatcher();
2239
- if (onThemeChangeCallback) {
2240
- onThemeChangeCallback();
2241
- }
2245
+ notifyThemeChange();
2242
2246
  }
2243
2247
 
2244
2248
  /**
@@ -2259,7 +2263,7 @@ export async function setSymbolPreset(preset: SymbolPreset): Promise<void> {
2259
2263
  theme = await loadTheme("dark", getCurrentThemeOptions());
2260
2264
  if (requestId !== themeLoadRequestId) return;
2261
2265
  }
2262
- onThemeChangeCallback?.();
2266
+ notifyThemeChange();
2263
2267
  }
2264
2268
 
2265
2269
  /**
@@ -2288,7 +2292,7 @@ export async function setColorBlindMode(enabled: boolean): Promise<void> {
2288
2292
  theme = await loadTheme("dark", getCurrentThemeOptions());
2289
2293
  if (requestId !== themeLoadRequestId) return;
2290
2294
  }
2291
- onThemeChangeCallback?.();
2295
+ notifyThemeChange();
2292
2296
  }
2293
2297
 
2294
2298
  /**
@@ -2302,6 +2306,23 @@ export function onThemeChange(callback: () => void): void {
2302
2306
  onThemeChangeCallback = callback;
2303
2307
  }
2304
2308
 
2309
+ /**
2310
+ * Monotonic counter bumped on any theme-affecting change that should invalidate
2311
+ * cached renders: theme swaps and reloads (including the invalid-theme dark
2312
+ * fallback), theme previews, symbol-preset changes, and color-blind-mode
2313
+ * changes — everything that routes through {@link notifyThemeChange}. Consumers
2314
+ * key cached renders on it so the next render re-shapes their output.
2315
+ */
2316
+ export function getThemeEpoch(): number {
2317
+ return themeEpoch;
2318
+ }
2319
+
2320
+ /** Bump the theme epoch and notify the registered theme-change listener. */
2321
+ function notifyThemeChange(): void {
2322
+ themeEpoch++;
2323
+ onThemeChangeCallback?.();
2324
+ }
2325
+
2305
2326
  /**
2306
2327
  * Get available symbol presets.
2307
2328
  */
@@ -2354,9 +2375,7 @@ async function startThemeWatcher(): Promise<void> {
2354
2375
  loadTheme(watchedThemeName, getCurrentThemeOptions())
2355
2376
  .then(loadedTheme => {
2356
2377
  theme = loadedTheme;
2357
- if (onThemeChangeCallback) {
2358
- onThemeChangeCallback();
2359
- }
2378
+ notifyThemeChange();
2360
2379
  })
2361
2380
  .catch(() => {
2362
2381
  // Ignore errors (file might be in invalid state while being edited)
@@ -2396,9 +2415,7 @@ function reevaluateAutoTheme(debugLabel: string): void {
2396
2415
  loadTheme(resolved, getCurrentThemeOptions())
2397
2416
  .then(loadedTheme => {
2398
2417
  theme = loadedTheme;
2399
- if (onThemeChangeCallback) {
2400
- onThemeChangeCallback();
2401
- }
2418
+ notifyThemeChange();
2402
2419
  })
2403
2420
  .catch(err => {
2404
2421
  logger.debug(`Theme switch on ${debugLabel} failed`, { error: String(err) });
@@ -2520,18 +2537,74 @@ function ansi256ToHex(index: number): string {
2520
2537
  return `#${grayHex}${grayHex}${grayHex}`;
2521
2538
  }
2522
2539
 
2540
+ /**
2541
+ * Classify a parsed theme JSON as light/dark by the perceived luminance of its
2542
+ * status-line background. Mirrors {@link Theme.isLight} so the synchronous
2543
+ * helpers below stay in lockstep with the runtime classifier — see the comment
2544
+ * on `Theme.statusLineLuminance` for why `statusLineBg` is the source of truth
2545
+ * (themes like `porcelain` style a dark chat bubble on an otherwise-light
2546
+ * theme, so `userMessageBg` is unreliable).
2547
+ */
2548
+ function isLightThemeJson(themeJson: ThemeJson): boolean {
2549
+ try {
2550
+ const resolved = resolveVarRefs(themeJson.colors.statusLineBg, themeJson.vars ?? {});
2551
+ const luminance = colorLuma(resolved);
2552
+ return luminance !== undefined && luminance > 0.5;
2553
+ } catch {
2554
+ return false;
2555
+ }
2556
+ }
2557
+
2558
+ function getHtmlDefaultTextForSurface(surface: string | number | undefined): string {
2559
+ const luminance = surface === undefined ? undefined : colorLuma(surface);
2560
+ return luminance !== undefined && luminance > 0.5 ? "#000000" : "#e5e5e7";
2561
+ }
2562
+
2563
+ function resolveThemeExportColors(themeJson: ThemeJson): {
2564
+ pageBg?: string;
2565
+ cardBg?: string;
2566
+ infoBg?: string;
2567
+ } {
2568
+ const exportSection = themeJson.export;
2569
+ if (!exportSection) return {};
2570
+
2571
+ const vars = themeJson.vars ?? {};
2572
+ const resolve = (value: string | number | undefined): string | undefined => {
2573
+ if (value === undefined) return undefined;
2574
+ if (typeof value === "number") return ansi256ToHex(value);
2575
+ if (value === "" || value.startsWith("#")) return value;
2576
+ const varName = value.startsWith("$") ? value.slice(1) : value;
2577
+ if (varName in vars) {
2578
+ const resolved = resolveVarRefs(varName, vars);
2579
+ return typeof resolved === "number" ? ansi256ToHex(resolved) : resolved;
2580
+ }
2581
+ return value;
2582
+ };
2583
+
2584
+ return {
2585
+ pageBg: resolve(exportSection.pageBg),
2586
+ cardBg: resolve(exportSection.cardBg),
2587
+ infoBg: resolve(exportSection.infoBg),
2588
+ };
2589
+ }
2590
+
2523
2591
  /**
2524
2592
  * Get resolved theme colors as CSS-compatible hex strings.
2525
2593
  * Used by HTML export to generate CSS custom properties.
2526
2594
  */
2527
2595
  export async function getResolvedThemeColors(themeName?: string): Promise<Record<string, string>> {
2528
2596
  const name = themeName ?? getDefaultTheme();
2529
- const isLight = name === "light";
2530
2597
  const themeJson = await loadThemeJson(name);
2598
+ const exportColors = resolveThemeExportColors(themeJson);
2531
2599
  const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
2532
2600
 
2533
- // Default text color for empty values (terminal uses default fg color)
2534
- const defaultText = isLight ? "#000000" : "#e5e5e7";
2601
+ // Empty foreground tokens use the terminal default color. In HTML export,
2602
+ // that default must contrast the export surface, not the TUI status line:
2603
+ // custom light themes can still export dark transcript cards when they omit
2604
+ // `export`, because generateThemeVars derives those cards from userMessageBg.
2605
+ const defaultText = getHtmlDefaultTextForSurface(
2606
+ exportColors.cardBg ?? exportColors.pageBg ?? resolved.userMessageBg,
2607
+ );
2535
2608
 
2536
2609
  const cssColors: Record<string, string> = {};
2537
2610
  for (const [key, value] of Object.entries(resolved)) {
@@ -2548,8 +2621,9 @@ export async function getResolvedThemeColors(themeName?: string): Promise<Record
2548
2621
  }
2549
2622
 
2550
2623
  /**
2551
- * Check if a theme is a "light" theme by analyzing its background color luminance.
2552
- * Loads theme JSON synchronously (built-in or custom file) and resolves userMessageBg.
2624
+ * Check if a theme is a "light" theme by analyzing its status-line background
2625
+ * luminance. Loads theme JSON synchronously (built-in or custom file on disk)
2626
+ * for callers in synchronous flows (settings migration, setup wizard).
2553
2627
  */
2554
2628
  export function isLightTheme(themeName?: string): boolean {
2555
2629
  const name = themeName ?? "dark";
@@ -2566,13 +2640,7 @@ export function isLightTheme(themeName?: string): boolean {
2566
2640
  return false;
2567
2641
  }
2568
2642
  }
2569
- try {
2570
- const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
2571
- const luminance = colorLuma(resolved);
2572
- return luminance !== undefined && luminance > 0.5;
2573
- } catch {
2574
- return false;
2575
- }
2643
+ return isLightThemeJson(themeJson);
2576
2644
  }
2577
2645
 
2578
2646
  /**
@@ -2587,27 +2655,7 @@ export async function getThemeExportColors(themeName?: string): Promise<{
2587
2655
  const name = themeName ?? getDefaultTheme();
2588
2656
  try {
2589
2657
  const themeJson = await loadThemeJson(name);
2590
- const exportSection = themeJson.export;
2591
- if (!exportSection) return {};
2592
-
2593
- const vars = themeJson.vars ?? {};
2594
- const resolve = (value: string | number | undefined): string | undefined => {
2595
- if (value === undefined) return undefined;
2596
- if (typeof value === "number") return ansi256ToHex(value);
2597
- if (value === "" || value.startsWith("#")) return value;
2598
- const varName = value.startsWith("$") ? value.slice(1) : value;
2599
- if (varName in vars) {
2600
- const resolved = resolveVarRefs(varName, vars);
2601
- return typeof resolved === "number" ? ansi256ToHex(resolved) : resolved;
2602
- }
2603
- return value;
2604
- };
2605
-
2606
- return {
2607
- pageBg: resolve(exportSection.pageBg),
2608
- cardBg: resolve(exportSection.cardBg),
2609
- infoBg: resolve(exportSection.infoBg),
2610
- };
2658
+ return resolveThemeExportColors(themeJson);
2611
2659
  } catch {
2612
2660
  return {};
2613
2661
  }
@@ -18,7 +18,8 @@ import type { MCPManager } from "../mcp";
18
18
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
19
19
  import type { AgentSession } from "../session/agent-session";
20
20
  import type { HistoryStorage } from "../session/history-storage";
21
- import type { SessionContext, SessionManager } from "../session/session-manager";
21
+ import type { SessionContext } from "../session/session-context";
22
+ import type { SessionManager } from "../session/session-manager";
22
23
  import type { ShakeMode } from "../session/shake-types";
23
24
  import type { LspStartupServerInfo } from "../tools";
24
25
  import type { EventBus } from "../utils/event-bus";
@@ -89,6 +90,7 @@ export interface InteractiveModeContext {
89
90
  btwContainer: Container;
90
91
  omfgContainer: Container;
91
92
  errorBannerContainer: Container;
93
+ modelCycleContainer: Container;
92
94
  editor: CustomEditor;
93
95
  editorContainer: Container;
94
96
  hookWidgetContainerAbove: Container;
@@ -196,6 +198,7 @@ export interface InteractiveModeContext {
196
198
  */
197
199
  resetTranscript(): void;
198
200
  showStatus(message: string, options?: { dim?: boolean }): void;
201
+ showModelCycleTrack(track: string): void;
199
202
  showError(message: string): void;
200
203
  showPinnedError(message: string): void;
201
204
  clearPinnedError(): void;
@@ -307,7 +310,7 @@ export interface InteractiveModeContext {
307
310
  showProviderSetup(): Promise<void>;
308
311
  showHookConfirm(title: string, message: string): Promise<boolean>;
309
312
  showDebugSelector(): Promise<void>;
310
- showAgentHub(): void;
313
+ showAgentHub(options?: { requireContent?: boolean }): void;
311
314
  resetObserverRegistry(): void;
312
315
 
313
316
  // Input handling
@@ -332,6 +335,7 @@ export interface InteractiveModeContext {
332
335
  registerExtensionShortcuts(): void;
333
336
  handlePlanModeCommand(initialPrompt?: string): Promise<void>;
334
337
  handleGoalModeCommand(rest?: string): Promise<void>;
338
+ handleGuidedGoalCommand(rest?: string): Promise<void>;
335
339
  handleLoopCommand(args?: string): Promise<void>;
336
340
  disableLoopMode(): void;
337
341
  pauseLoop(): void;
@@ -49,7 +49,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
49
49
  `| \`${appKey(bindings, "app.thinking.toggle")}\` | Toggle thinking block visibility |`,
50
50
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
51
51
  `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image or text from clipboard |`,
52
- `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
52
+ "| Hold `Space` | Speech-to-text (push-to-talk): hold to record, release to transcribe |",
53
53
  `| \`${appKey(bindings, "app.agents.hub")}\` / \`${appKey(bindings, "app.session.observe")}\` / double-tap \`←\` (empty editor) | Open the agent hub |`,
54
54
  "| `#` | Open prompt actions |",
55
55
  "| `/` | Slash commands |",