@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -40,6 +40,7 @@ import { shortenPath } from "../../tools/render-utils";
40
40
  import { copyToClipboard } from "../../utils/clipboard";
41
41
  import { setSessionTerminalTitle } from "../../utils/title-generator";
42
42
  import { AgentDashboard } from "../components/agent-dashboard";
43
+ import { AgentHubOverlayComponent } from "../components/agent-hub";
43
44
  import { AssistantMessageComponent } from "../components/assistant-message";
44
45
  import { CopySelectorComponent } from "../components/copy-selector";
45
46
  import { ExtensionDashboard } from "../components/extensions";
@@ -47,7 +48,6 @@ import { HistorySearchComponent } from "../components/history-search";
47
48
  import { ModelSelectorComponent } from "../components/model-selector";
48
49
  import { OAuthSelectorComponent } from "../components/oauth-selector";
49
50
  import { PluginSelectorComponent } from "../components/plugin-selector";
50
- import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
51
51
  import { SessionSelectorComponent } from "../components/session-selector";
52
52
  import { SettingsSelectorComponent } from "../components/settings-selector";
53
53
  import { ToolExecutionComponent } from "../components/tool-execution";
@@ -578,7 +578,7 @@ export class SelectorController {
578
578
  }
579
579
 
580
580
  this.ctx.chatContainer.clear();
581
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
581
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
582
582
  this.ctx.editor.setText(result.selectedText);
583
583
  done();
584
584
  this.ctx.showStatus("Branched to new session");
@@ -719,9 +719,10 @@ export class SelectorController {
719
719
  return;
720
720
  }
721
721
 
722
- // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
722
+ // Update UI — rebuild the display transcript for the new leaf (the
723
+ // context from navigateTree is the LLM context, not the transcript).
723
724
  this.ctx.chatContainer.clear();
724
- this.ctx.renderInitialMessages(result.sessionContext, { clearTerminalHistory: true });
725
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
725
726
  await this.ctx.reloadTodos();
726
727
  if (result.editorText && !this.ctx.editor.getText().trim()) {
727
728
  this.ctx.editor.setText(result.editorText);
@@ -846,7 +847,7 @@ export class SelectorController {
846
847
  this.ctx.statusLine.setSessionStartTime(Date.now());
847
848
  this.ctx.updateEditorTopBorder();
848
849
  this.ctx.updateEditorBorderColor();
849
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
850
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
850
851
  await this.ctx.reloadTodos();
851
852
  this.ctx.ui.requestRender(true, { clearScrollback: true });
852
853
  return true;
@@ -871,7 +872,7 @@ export class SelectorController {
871
872
 
872
873
  // Clear and re-render the chat
873
874
  this.ctx.chatContainer.clear();
874
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
875
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
875
876
  await this.ctx.reloadTodos();
876
877
  this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
877
878
  }
@@ -1074,31 +1075,34 @@ export class SelectorController {
1074
1075
  });
1075
1076
  }
1076
1077
 
1077
- showSessionObserver(registry: SessionObserverRegistry): void {
1078
- const observeKeys = this.ctx.keybindings.getKeys("app.session.observe");
1079
- let cleanup: (() => void) | undefined;
1078
+ showAgentHub(observers: SessionObserverRegistry): void {
1079
+ const hubKeys = [
1080
+ ...this.ctx.keybindings.getKeys("app.agents.hub"),
1081
+ ...this.ctx.keybindings.getKeys("app.session.observe"),
1082
+ ];
1083
+ let hub: AgentHubOverlayComponent | undefined;
1080
1084
  let overlayHandle: OverlayHandle | undefined;
1081
1085
 
1082
1086
  const done = () => {
1083
- cleanup?.();
1087
+ hub?.dispose();
1084
1088
  overlayHandle?.hide();
1085
1089
  this.ctx.ui.requestRender();
1086
1090
  };
1087
1091
 
1088
- const selector = new SessionObserverOverlayComponent(registry, done, observeKeys);
1089
-
1090
- cleanup = registry.onChange(() => {
1091
- selector.refreshFromRegistry();
1092
- this.ctx.ui.requestRender();
1092
+ hub = new AgentHubOverlayComponent({
1093
+ observers,
1094
+ hubKeys,
1095
+ onDone: done,
1096
+ requestRender: () => this.ctx.ui.requestRender(),
1093
1097
  });
1094
1098
 
1095
- overlayHandle = this.ctx.ui.showOverlay(selector, {
1099
+ overlayHandle = this.ctx.ui.showOverlay(hub, {
1096
1100
  anchor: "bottom-center",
1097
1101
  width: "100%",
1098
1102
  maxHeight: "100%",
1099
1103
  margin: 0,
1100
1104
  });
1101
- this.ctx.ui.setFocus(selector);
1105
+ this.ctx.ui.setFocus(hub);
1102
1106
  this.ctx.ui.requestRender();
1103
1107
  }
1104
1108
  }
@@ -8,27 +8,9 @@ import { postmortem } from "@oh-my-pi/pi-utils";
8
8
  * barrel does not pull print, RPC server, or ACP server mode into the normal
9
9
  * TUI graph.
10
10
  */
11
- export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
12
- export {
13
- defineRpcClientTool,
14
- type ModelInfo,
15
- RpcClient,
16
- type RpcClientCustomTool,
17
- type RpcClientOptions,
18
- type RpcClientToolContext,
19
- type RpcClientToolResult,
20
- type RpcEventListener,
21
- } from "./rpc/rpc-client";
22
- export type {
23
- RpcCommand,
24
- RpcHostToolCallRequest,
25
- RpcHostToolCancelRequest,
26
- RpcHostToolDefinition,
27
- RpcHostToolResult,
28
- RpcHostToolUpdate,
29
- RpcResponse,
30
- RpcSessionState,
31
- } from "./rpc/rpc-types";
11
+ export * from "./interactive-mode";
12
+ export * from "./rpc/rpc-client";
13
+ export * from "./rpc/rpc-types";
32
14
 
33
15
  postmortem.register("terminal-restore", () => {
34
16
  emergencyTerminalRestore();
@@ -59,6 +59,7 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
59
59
  import type { Goal, GoalModeState } from "../goals/state";
60
60
  import { resolveLocalUrlToPath } from "../internal-urls";
61
61
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
62
+ import type { MCPManager } from "../mcp";
62
63
  import {
63
64
  humanizePlanTitle,
64
65
  type PlanApprovalDetails,
@@ -74,8 +75,10 @@ import { HistoryStorage } from "../session/history-storage";
74
75
  import type { SessionContext, SessionManager } from "../session/session-manager";
75
76
  import { getRecentSessions } from "../session/session-manager";
76
77
  import type { ShakeMode } from "../session/shake-types";
78
+ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-registry";
77
79
  import { formatDuration } from "../slash-commands/helpers/format";
78
80
  import { STTController, type SttState } from "../stt";
81
+ import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
79
82
  import type { LspStartupServerInfo } from "../tools";
80
83
  import { normalizeLocalScheme } from "../tools/path-utils";
81
84
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
@@ -123,6 +126,7 @@ import {
123
126
  } from "./loop-limit";
124
127
  import { OAuthManualInputManager } from "./oauth-manual-input";
125
128
  import { SessionObserverRegistry } from "./session-observer-registry";
129
+ import { runProviderSetupWizard } from "./setup-wizard/lazy";
126
130
  import { interruptHint } from "./shared";
127
131
  import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
128
132
  import type { Theme } from "./theme/theme";
@@ -260,6 +264,7 @@ export class InteractiveMode implements InteractiveModeContext {
260
264
  keybindings: KeybindingsManager;
261
265
  agent: Agent;
262
266
  historyStorage?: HistoryStorage;
267
+ titleSystemPrompt?: string;
263
268
 
264
269
  ui: TUI;
265
270
  chatContainer: TranscriptContainer;
@@ -322,6 +327,7 @@ export class InteractiveMode implements InteractiveModeContext {
322
327
  #pendingSubmissionDispose: (() => void) | undefined;
323
328
  lastSigintTime = 0;
324
329
  lastEscapeTime = 0;
330
+ lastLeftTapTime = 0;
325
331
  shutdownRequested = false;
326
332
  #isShuttingDown = false;
327
333
  hookSelector: HookSelectorComponent | undefined = undefined;
@@ -349,7 +355,7 @@ export class InteractiveMode implements InteractiveModeContext {
349
355
  #planReviewOverlay: PlanReviewOverlay | undefined;
350
356
  #planReviewOverlayHandle: OverlayHandle | undefined;
351
357
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
352
- mcpManager?: import("../mcp").MCPManager;
358
+ mcpManager?: MCPManager;
353
359
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
354
360
 
355
361
  readonly #btwController: BtwController;
@@ -380,8 +386,9 @@ export class InteractiveMode implements InteractiveModeContext {
380
386
  changelogMarkdown: string | undefined = undefined,
381
387
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
382
388
  lspServers: LspStartupServerInfo[] | undefined = undefined,
383
- mcpManager?: import("../mcp").MCPManager,
389
+ mcpManager?: MCPManager,
384
390
  eventBus?: EventBus,
391
+ titleSystemPrompt?: string,
385
392
  ) {
386
393
  this.session = session;
387
394
  this.sessionManager = session.sessionManager;
@@ -394,6 +401,7 @@ export class InteractiveMode implements InteractiveModeContext {
394
401
  this.lspServers = lspServers;
395
402
  this.mcpManager = mcpManager;
396
403
  this.#eventBus = eventBus;
404
+ this.titleSystemPrompt = titleSystemPrompt;
397
405
  if (eventBus) {
398
406
  this.#eventBusUnsubscribers.push(
399
407
  eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
@@ -447,9 +455,8 @@ export class InteractiveMode implements InteractiveModeContext {
447
455
 
448
456
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
449
457
 
450
- const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
451
458
  const hookCommands: SlashCommand[] = (
452
- this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []
459
+ this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
453
460
  ).map(cmd => ({
454
461
  name: cmd.name,
455
462
  description: cmd.description ?? "(hook command)",
@@ -679,6 +686,13 @@ export class InteractiveMode implements InteractiveModeContext {
679
686
  this.updateEditorTopBorder();
680
687
  }
681
688
 
689
+ /** Reload the title-generation system prompt override for the provided working directory. */
690
+ async refreshTitleSystemPrompt(cwd?: string): Promise<void> {
691
+ const basePath = cwd ?? this.sessionManager.getCwd();
692
+ const titleSystemPromptSource = discoverTitleSystemPromptFile(basePath);
693
+ this.titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
694
+ }
695
+
682
696
  /** Reload slash commands and autocomplete for the provided working directory. */
683
697
  async refreshSlashCommandState(cwd?: string): Promise<void> {
684
698
  const basePath = cwd ?? this.sessionManager.getCwd();
@@ -713,6 +727,7 @@ export class InteractiveMode implements InteractiveModeContext {
713
727
  // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
714
728
  // the next prompt sees everything scoped to the new project directory.
715
729
  clearClaudePluginRootsCache();
730
+ await this.refreshTitleSystemPrompt(newCwd);
716
731
  resetCapabilities();
717
732
  await this.refreshSlashCommandState(newCwd);
718
733
  await this.session.refreshSshTool({ activateIfAvailable: true });
@@ -1074,7 +1089,9 @@ export class InteractiveMode implements InteractiveModeContext {
1074
1089
 
1075
1090
  rebuildChatFromMessages(): void {
1076
1091
  this.chatContainer.clear();
1077
- const context = this.session.buildDisplaySessionContext();
1092
+ // Full-history transcript: compactions render as inline dividers instead
1093
+ // of restarting the visible conversation (the LLM context still resets).
1094
+ const context = this.session.buildTranscriptSessionContext();
1078
1095
  this.renderSessionContext(context);
1079
1096
  }
1080
1097
 
@@ -1694,6 +1711,15 @@ export class InteractiveMode implements InteractiveModeContext {
1694
1711
  }
1695
1712
  }
1696
1713
 
1714
+ async #hasPlanModeDraftContent(planFilePath: string): Promise<boolean> {
1715
+ const candidates = new Set<string>([planFilePath, ...(await this.#listLocalPlanFiles())]);
1716
+ for (const candidate of candidates) {
1717
+ const content = await this.#readPlanFile(candidate);
1718
+ if (content !== null && content.trim().length > 0) return true;
1719
+ }
1720
+ return false;
1721
+ }
1722
+
1697
1723
  /** `local://` URLs of plan files in the session-local root, newest first.
1698
1724
  * A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
1699
1725
  * so the plan it wrote is still found by scanning recent `*-plan.md` files. */
@@ -1998,11 +2024,14 @@ export class InteractiveMode implements InteractiveModeContext {
1998
2024
  return;
1999
2025
  }
2000
2026
  if (this.planModeEnabled) {
2001
- const confirmed = await this.showHookConfirm(
2002
- "Exit plan mode?",
2003
- "This exits plan mode without approving a plan.",
2004
- );
2005
- if (!confirmed) return;
2027
+ const planFilePath = this.planModePlanFilePath ?? (await this.#getPlanFilePath());
2028
+ if (await this.#hasPlanModeDraftContent(planFilePath)) {
2029
+ const confirmed = await this.showHookConfirm(
2030
+ "Exit plan mode?",
2031
+ "This exits plan mode without approving a plan.",
2032
+ );
2033
+ if (!confirmed) return;
2034
+ }
2006
2035
  await this.#exitPlanMode({ paused: true });
2007
2036
  return;
2008
2037
  }
@@ -2854,11 +2883,8 @@ export class InteractiveMode implements InteractiveModeContext {
2854
2883
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2855
2884
  }
2856
2885
 
2857
- renderInitialMessages(
2858
- prebuiltContext?: SessionContext,
2859
- options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
2860
- ): void {
2861
- this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2886
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void {
2887
+ this.#uiHelpers.renderInitialMessages(options);
2862
2888
  }
2863
2889
 
2864
2890
  getUserMessageText(message: Message): string {
@@ -3042,13 +3068,8 @@ export class InteractiveMode implements InteractiveModeContext {
3042
3068
  await this.#selectorController.showDebugSelector();
3043
3069
  }
3044
3070
 
3045
- showSessionObserver(): void {
3046
- const sessions = this.#observerRegistry.getSessions();
3047
- if (sessions.length <= 1) {
3048
- this.showStatus("No active subagent sessions");
3049
- return;
3050
- }
3051
- this.#selectorController.showSessionObserver(this.#observerRegistry);
3071
+ showAgentHub(): void {
3072
+ this.#selectorController.showAgentHub(this.#observerRegistry);
3052
3073
  }
3053
3074
 
3054
3075
  resetObserverRegistry(): void {
@@ -3153,6 +3174,10 @@ export class InteractiveMode implements InteractiveModeContext {
3153
3174
  return this.#selectorController.showOAuthSelector(mode, providerId);
3154
3175
  }
3155
3176
 
3177
+ showProviderSetup(): Promise<void> {
3178
+ return runProviderSetupWizard(this);
3179
+ }
3180
+
3156
3181
  showHookConfirm(title: string, message: string): Promise<boolean> {
3157
3182
  return this.#extensionUiController.showHookConfirm(title, message);
3158
3183
  }
@@ -3,6 +3,10 @@ type PendingInput = {
3
3
  resolve: (value: string) => void;
4
4
  reject: (error: Error) => void;
5
5
  };
6
+ type ClaimedInput = {
7
+ promise: Promise<string>;
8
+ clear: (reason?: string) => void;
9
+ };
6
10
 
7
11
  export class OAuthManualInputManager {
8
12
  #pending?: PendingInput;
@@ -12,9 +16,27 @@ export class OAuthManualInputManager {
12
16
  this.clear("Manual OAuth input superseded by a new login");
13
17
  }
14
18
 
15
- const { promise, resolve, reject } = Promise.withResolvers<string>();
16
- this.#pending = { providerId, resolve, reject };
17
- return promise;
19
+ const pending = this.#createPending(providerId);
20
+ this.#pending = pending;
21
+ return pending.promise;
22
+ }
23
+
24
+ tryWaitForInput(providerId: string): Promise<string> | undefined {
25
+ if (this.#pending) return undefined;
26
+ return this.waitForInput(providerId);
27
+ }
28
+
29
+ tryClaimInput(providerId: string): ClaimedInput | undefined {
30
+ if (this.#pending) return undefined;
31
+ const pending = this.#createPending(providerId);
32
+ this.#pending = pending;
33
+ return {
34
+ promise: pending.promise,
35
+ clear: (reason?: string) => {
36
+ if (this.#pending !== pending) return;
37
+ this.clear(reason);
38
+ },
39
+ };
18
40
  }
19
41
 
20
42
  submit(input: string): boolean {
@@ -39,4 +61,9 @@ export class OAuthManualInputManager {
39
61
  get pendingProviderId(): string | undefined {
40
62
  return this.#pending?.providerId;
41
63
  }
64
+
65
+ #createPending(providerId: string): PendingInput & { promise: Promise<string> } {
66
+ const { promise, resolve, reject } = Promise.withResolvers<string>();
67
+ return { providerId, resolve, reject, promise };
68
+ }
42
69
  }
@@ -9,8 +9,9 @@ import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@
9
9
  import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
10
10
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
11
11
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
12
+ import type { FileSink } from "bun";
12
13
  import type { BashResult } from "../../exec/bash-executor";
13
- import type { SessionStats } from "../../session/agent-session";
14
+ import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
14
15
  import type {
15
16
  RpcCommand,
16
17
  RpcExtensionUIRequest,
@@ -22,6 +23,12 @@ import type {
22
23
  RpcHostToolUpdate,
23
24
  RpcResponse,
24
25
  RpcSessionState,
26
+ RpcSubagentEventFrame,
27
+ RpcSubagentLifecycleFrame,
28
+ RpcSubagentMessagesResult,
29
+ RpcSubagentProgressFrame,
30
+ RpcSubagentSnapshot,
31
+ RpcSubagentSubscriptionLevel,
25
32
  } from "./rpc-types";
26
33
 
27
34
  /** Distributive Omit that works with union types */
@@ -52,6 +59,10 @@ export interface RpcClientOptions {
52
59
  export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
53
60
 
54
61
  export type RpcEventListener = (event: AgentEvent) => void;
62
+ export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
63
+ export type RpcSubagentLifecycleListener = (payload: RpcSubagentLifecycleFrame["payload"]) => void;
64
+ export type RpcSubagentProgressListener = (payload: RpcSubagentProgressFrame["payload"]) => void;
65
+ export type RpcSubagentEventListener = (payload: RpcSubagentEventFrame["payload"]) => void;
55
66
 
56
67
  export interface RpcClientToolContext<TDetails = unknown> {
57
68
  toolCallId: string;
@@ -92,6 +103,23 @@ const agentEventTypes = new Set<AgentEvent["type"]>([
92
103
  "tool_execution_end",
93
104
  ]);
94
105
 
106
+ const sessionEventTypes = new Set<AgentSessionEvent["type"]>([
107
+ ...agentEventTypes,
108
+ "auto_compaction_start",
109
+ "auto_compaction_end",
110
+ "auto_retry_start",
111
+ "auto_retry_end",
112
+ "retry_fallback_applied",
113
+ "retry_fallback_succeeded",
114
+ "ttsr_triggered",
115
+ "todo_reminder",
116
+ "todo_auto_clear",
117
+ "irc_message",
118
+ "notice",
119
+ "thinking_level_changed",
120
+ "goal_updated",
121
+ ]);
122
+
95
123
  function isRpcResponse(value: unknown): value is RpcResponse {
96
124
  if (!isRecord(value)) return false;
97
125
  if (value.type !== "response") return false;
@@ -111,6 +139,28 @@ function isAgentEvent(value: unknown): value is AgentEvent {
111
139
  return agentEventTypes.has(type as AgentEvent["type"]);
112
140
  }
113
141
 
142
+ function isAgentSessionEvent(value: unknown): value is AgentSessionEvent {
143
+ if (!isRecord(value)) return false;
144
+ const type = value.type;
145
+ if (typeof type !== "string") return false;
146
+ return sessionEventTypes.has(type as AgentSessionEvent["type"]);
147
+ }
148
+
149
+ function isRpcSubagentLifecycleFrame(value: unknown): value is RpcSubagentLifecycleFrame {
150
+ if (!isRecord(value)) return false;
151
+ return value.type === "subagent_lifecycle" && isRecord(value.payload);
152
+ }
153
+
154
+ function isRpcSubagentProgressFrame(value: unknown): value is RpcSubagentProgressFrame {
155
+ if (!isRecord(value)) return false;
156
+ return value.type === "subagent_progress" && isRecord(value.payload);
157
+ }
158
+
159
+ function isRpcSubagentEventFrame(value: unknown): value is RpcSubagentEventFrame {
160
+ if (!isRecord(value)) return false;
161
+ return value.type === "subagent_event" && isRecord(value.payload);
162
+ }
163
+
114
164
  function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
115
165
  if (!isRecord(value)) return false;
116
166
  return (
@@ -148,6 +198,10 @@ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): A
148
198
  export class RpcClient {
149
199
  #process: ptree.ChildProcess | null = null;
150
200
  #eventListeners: RpcEventListener[] = [];
201
+ #sessionEventListeners: RpcSessionEventListener[] = [];
202
+ #subagentLifecycleListeners = new Set<RpcSubagentLifecycleListener>();
203
+ #subagentProgressListeners = new Set<RpcSubagentProgressListener>();
204
+ #subagentEventListeners = new Set<RpcSubagentEventListener>();
151
205
  #pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
152
206
  new Map();
153
207
  #customTools: RpcClientCustomTool[] = [];
@@ -286,6 +340,43 @@ export class RpcClient {
286
340
  };
287
341
  }
288
342
 
343
+ /**
344
+ * Subscribe to all top-level session events, including non-core session state events.
345
+ */
346
+ onSessionEvent(listener: RpcSessionEventListener): () => void {
347
+ this.#sessionEventListeners.push(listener);
348
+ return () => {
349
+ const index = this.#sessionEventListeners.indexOf(listener);
350
+ if (index !== -1) {
351
+ this.#sessionEventListeners.splice(index, 1);
352
+ }
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Subscribe to subagent lifecycle frames after setSubagentSubscription("progress" | "events").
358
+ */
359
+ onSubagentLifecycle(listener: RpcSubagentLifecycleListener): () => void {
360
+ this.#subagentLifecycleListeners.add(listener);
361
+ return () => this.#subagentLifecycleListeners.delete(listener);
362
+ }
363
+
364
+ /**
365
+ * Subscribe to aggregated subagent progress frames after setSubagentSubscription("progress" | "events").
366
+ */
367
+ onSubagentProgress(listener: RpcSubagentProgressListener): () => void {
368
+ this.#subagentProgressListeners.add(listener);
369
+ return () => this.#subagentProgressListeners.delete(listener);
370
+ }
371
+
372
+ /**
373
+ * Subscribe to raw subagent session events. Call setSubagentSubscription(\"events\") to enable them server-side.
374
+ */
375
+ onSubagentEvent(listener: RpcSubagentEventListener): () => void {
376
+ this.#subagentEventListeners.add(listener);
377
+ return () => this.#subagentEventListeners.delete(listener);
378
+ }
379
+
289
380
  /**
290
381
  * Get collected stderr output (useful for debugging).
291
382
  */
@@ -358,6 +449,40 @@ export class RpcClient {
358
449
  return this.#getData(response);
359
450
  }
360
451
 
452
+ /**
453
+ * Configure subagent frames emitted by the RPC server. Servers default to "off".
454
+ * "progress" emits lifecycle/progress frames; "events" additionally emits raw subagent session events.
455
+ */
456
+ async setSubagentSubscription(level: RpcSubagentSubscriptionLevel): Promise<RpcSubagentSubscriptionLevel> {
457
+ const response = await this.#send({ type: "set_subagent_subscription", level });
458
+ return this.#getData<{ level: RpcSubagentSubscriptionLevel }>(response).level;
459
+ }
460
+
461
+ /**
462
+ * Return the RPC server's current subagent snapshot.
463
+ */
464
+ async getSubagents(): Promise<RpcSubagentSnapshot[]> {
465
+ const response = await this.#send({ type: "get_subagents" });
466
+ return this.#getData<{ subagents: RpcSubagentSnapshot[] }>(response).subagents;
467
+ }
468
+
469
+ /**
470
+ * Read persisted transcript entries for a tracked subagent session.
471
+ */
472
+ async getSubagentMessages(selector: {
473
+ subagentId?: string;
474
+ sessionFile?: string;
475
+ fromByte?: number;
476
+ }): Promise<RpcSubagentMessagesResult> {
477
+ const response = await this.#send({
478
+ type: "get_subagent_messages",
479
+ subagentId: selector.subagentId,
480
+ sessionFile: selector.sessionFile,
481
+ fromByte: selector.fromByte,
482
+ });
483
+ return this.#getData<RpcSubagentMessagesResult>(response);
484
+ }
485
+
361
486
  /**
362
487
  * Set model by provider and ID.
363
488
  */
@@ -679,9 +804,35 @@ export class RpcClient {
679
804
  return;
680
805
  }
681
806
 
807
+ if (isRpcSubagentLifecycleFrame(data)) {
808
+ for (const listener of this.#subagentLifecycleListeners) {
809
+ listener(data.payload);
810
+ }
811
+ return;
812
+ }
813
+
814
+ if (isRpcSubagentProgressFrame(data)) {
815
+ for (const listener of this.#subagentProgressListeners) {
816
+ listener(data.payload);
817
+ }
818
+ return;
819
+ }
820
+
821
+ if (isRpcSubagentEventFrame(data)) {
822
+ for (const listener of this.#subagentEventListeners) {
823
+ listener(data.payload);
824
+ }
825
+ return;
826
+ }
827
+
828
+ if (!isAgentSessionEvent(data)) return;
829
+
830
+ for (const listener of this.#sessionEventListeners) {
831
+ listener(data);
832
+ }
833
+
682
834
  if (!isAgentEvent(data)) return;
683
835
 
684
- // Otherwise it's an event
685
836
  for (const listener of this.#eventListeners) {
686
837
  listener(data);
687
838
  }
@@ -789,7 +940,7 @@ export class RpcClient {
789
940
  if (!this.#process?.stdin) {
790
941
  throw new Error("Client not started");
791
942
  }
792
- const stdin = this.#process.stdin as import("bun").FileSink;
943
+ const stdin = this.#process.stdin as FileSink;
793
944
  stdin.write(`${JSON.stringify(frame)}\n`);
794
945
  const flushResult = stdin.flush();
795
946
  if (isPromise(flushResult)) {