@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.7

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 (201) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-1p8yvde7.md} +35 -1
  3. package/dist/cli.js +3340 -3339
  4. package/dist/types/advisor/advise-tool.d.ts +10 -4
  5. package/dist/types/autoresearch/tools/init-experiment.d.ts +19 -7
  6. package/dist/types/autoresearch/tools/log-experiment.d.ts +19 -11
  7. package/dist/types/autoresearch/tools/run-experiment.d.ts +4 -2
  8. package/dist/types/autoresearch/tools/update-notes.d.ts +5 -2
  9. package/dist/types/cli/gc-cli.d.ts +1 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +5 -2
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +5 -2
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +6 -2
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +5 -2
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +29 -7
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +14 -4
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +4 -2
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +7 -3
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +37 -10
  19. package/dist/types/commit/changelog/generate.d.ts +13 -3
  20. package/dist/types/commit/shared-llm.d.ts +13 -4
  21. package/dist/types/config/config-file.d.ts +1 -1
  22. package/dist/types/config/models-config-schema-bundle.d.ts +1992 -981
  23. package/dist/types/config/models-config-schema.d.ts +1201 -580
  24. package/dist/types/config/models-config.d.ts +355 -371
  25. package/dist/types/edit/hashline/params.d.ts +4 -2
  26. package/dist/types/edit/modes/apply-patch.d.ts +4 -2
  27. package/dist/types/edit/modes/patch.d.ts +16 -5
  28. package/dist/types/edit/modes/replace.d.ts +8 -3
  29. package/dist/types/eval/bridge-timeout.d.ts +5 -2
  30. package/dist/types/eval/completion-bridge.d.ts +13 -0
  31. package/dist/types/eval/py/tool-bridge.d.ts +12 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +4 -4
  33. package/dist/types/extensibility/custom-tools/types.d.ts +4 -4
  34. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  35. package/dist/types/extensibility/hooks/types.d.ts +4 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  38. package/dist/types/extensibility/legacy-typebox.d.ts +8 -0
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +6 -13
  40. package/dist/types/goals/tools/goal-tool.d.ts +12 -4
  41. package/dist/types/launch/client.d.ts +9 -1
  42. package/dist/types/launch/protocol.d.ts +17 -0
  43. package/dist/types/lsp/index.d.ts +15 -5
  44. package/dist/types/lsp/types.d.ts +15 -5
  45. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +4 -1
  49. package/dist/types/modes/types.d.ts +3 -1
  50. package/dist/types/security/contracts/schemas.d.ts +1115 -537
  51. package/dist/types/security/publication.d.ts +92 -34
  52. package/dist/types/session/agent-session-types.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +26 -2
  54. package/dist/types/session/launch-completion.d.ts +10 -0
  55. package/dist/types/session/session-entries.d.ts +15 -1
  56. package/dist/types/session/session-manager.d.ts +7 -0
  57. package/dist/types/session/yield-queue.d.ts +6 -1
  58. package/dist/types/task/types.d.ts +63 -18
  59. package/dist/types/tools/ask.d.ts +34 -8
  60. package/dist/types/tools/ast-edit.d.ts +16 -4
  61. package/dist/types/tools/ast-grep.d.ts +12 -4
  62. package/dist/types/tools/bash.d.ts +20 -11
  63. package/dist/types/tools/browser.d.ts +64 -20
  64. package/dist/types/tools/checkpoint.d.ts +16 -8
  65. package/dist/types/tools/computer.d.ts +1 -1
  66. package/dist/types/tools/debug.d.ts +118 -48
  67. package/dist/types/tools/eval.d.ts +11 -5
  68. package/dist/types/tools/gh.d.ts +74 -26
  69. package/dist/types/tools/glob.d.ts +16 -6
  70. package/dist/types/tools/grep.d.ts +18 -6
  71. package/dist/types/tools/hub/index.d.ts +116 -46
  72. package/dist/types/tools/image-gen.d.ts +25 -8
  73. package/dist/types/tools/index.d.ts +9 -0
  74. package/dist/types/tools/inspect-image.d.ts +10 -4
  75. package/dist/types/tools/learn.d.ts +26 -8
  76. package/dist/types/tools/manage-skill.d.ts +16 -6
  77. package/dist/types/tools/memory-edit.d.ts +18 -6
  78. package/dist/types/tools/memory-recall.d.ts +8 -4
  79. package/dist/types/tools/memory-reflect.d.ts +10 -4
  80. package/dist/types/tools/memory-retain.d.ts +14 -4
  81. package/dist/types/tools/read.d.ts +8 -4
  82. package/dist/types/tools/security-scan.d.ts +82 -30
  83. package/dist/types/tools/todo.d.ts +30 -10
  84. package/dist/types/tools/tts.d.ts +12 -5
  85. package/dist/types/tools/vibe.d.ts +42 -18
  86. package/dist/types/tools/write.d.ts +10 -4
  87. package/dist/types/web/search/index.d.ts +20 -6
  88. package/package.json +13 -13
  89. package/scripts/legacy-pi-virtual-module.ts +2 -2
  90. package/src/advisor/advise-tool.ts +1 -1
  91. package/src/advisor/config.ts +1 -1
  92. package/src/autoresearch/tools/init-experiment.ts +1 -2
  93. package/src/autoresearch/tools/log-experiment.ts +1 -2
  94. package/src/autoresearch/tools/run-experiment.ts +1 -1
  95. package/src/autoresearch/tools/update-notes.ts +1 -1
  96. package/src/cli/gc-cli.ts +641 -21
  97. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  98. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  99. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  100. package/src/commit/agentic/tools/git-overview.ts +1 -1
  101. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  102. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  103. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  104. package/src/commit/agentic/tools/schemas.ts +1 -1
  105. package/src/commit/agentic/tools/split-commit.ts +1 -1
  106. package/src/commit/analysis/summary.ts +1 -1
  107. package/src/commit/changelog/generate.ts +1 -1
  108. package/src/config/config-file.ts +2 -2
  109. package/src/config/model-discovery.ts +14 -14
  110. package/src/config/model-registry.ts +10 -8
  111. package/src/config/models-config-schema-bundle.ts +1 -8
  112. package/src/config/settings.ts +1 -1
  113. package/src/discovery/codex.ts +22 -9
  114. package/src/edit/hashline/params.ts +1 -1
  115. package/src/edit/modes/apply-patch.ts +1 -1
  116. package/src/edit/modes/patch.ts +1 -1
  117. package/src/edit/modes/replace.ts +2 -1
  118. package/src/eval/agent-bridge.ts +4 -2
  119. package/src/eval/bridge-timeout.ts +5 -2
  120. package/src/eval/completion-bridge.ts +3 -2
  121. package/src/eval/executor-base.ts +20 -1
  122. package/src/eval/js/context-manager.ts +94 -6
  123. package/src/eval/py/tool-bridge.ts +29 -9
  124. package/src/export/html/index.ts +10 -3
  125. package/src/export/share.ts +4 -0
  126. package/src/extensibility/custom-commands/loader.ts +3 -3
  127. package/src/extensibility/custom-commands/types.ts +4 -4
  128. package/src/extensibility/custom-tools/loader.ts +3 -3
  129. package/src/extensibility/custom-tools/types.ts +5 -4
  130. package/src/extensibility/extensions/loader.ts +3 -3
  131. package/src/extensibility/extensions/types.ts +5 -4
  132. package/src/extensibility/hooks/loader.ts +3 -3
  133. package/src/extensibility/hooks/types.ts +4 -4
  134. package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
  135. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  136. package/src/extensibility/legacy-typebox.ts +101 -0
  137. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -22
  138. package/src/goals/tools/goal-tool.ts +1 -1
  139. package/src/launch/broker.ts +222 -6
  140. package/src/launch/client.ts +161 -9
  141. package/src/launch/protocol.ts +52 -0
  142. package/src/lsp/types.ts +1 -1
  143. package/src/main.ts +13 -6
  144. package/src/mcp/config-writer.ts +1 -1
  145. package/src/modes/components/btw-panel.ts +41 -4
  146. package/src/modes/controllers/btw-controller.ts +55 -7
  147. package/src/modes/controllers/command-controller.ts +31 -0
  148. package/src/modes/controllers/input-controller.ts +24 -7
  149. package/src/modes/interactive-mode.ts +16 -2
  150. package/src/modes/theme/theme.ts +3 -3
  151. package/src/modes/types.ts +8 -1
  152. package/src/prompts/session/launch-completion.md +1 -0
  153. package/src/sdk.ts +15 -0
  154. package/src/security/contracts/schemas.ts +202 -184
  155. package/src/security/contracts/validation.ts +6 -2
  156. package/src/security/publication.ts +1 -1
  157. package/src/security/store.ts +2 -2
  158. package/src/session/agent-session-types.ts +6 -0
  159. package/src/session/agent-session.ts +190 -14
  160. package/src/session/launch-completion.ts +37 -0
  161. package/src/session/session-context.ts +26 -0
  162. package/src/session/session-entries.ts +17 -1
  163. package/src/session/session-manager.ts +21 -0
  164. package/src/session/yield-queue.ts +121 -16
  165. package/src/slash-commands/builtin-registry.ts +10 -1
  166. package/src/task/types.ts +1 -1
  167. package/src/tools/ask.ts +1 -1
  168. package/src/tools/ast-edit.ts +1 -1
  169. package/src/tools/ast-grep.ts +1 -1
  170. package/src/tools/bash.ts +61 -3
  171. package/src/tools/browser/cmux/cmux-tab.ts +92 -4
  172. package/src/tools/browser.ts +1 -1
  173. package/src/tools/checkpoint.ts +1 -1
  174. package/src/tools/computer.ts +1 -1
  175. package/src/tools/debug.ts +1 -1
  176. package/src/tools/eval.ts +1 -1
  177. package/src/tools/gh.ts +1 -2
  178. package/src/tools/glob.ts +1 -1
  179. package/src/tools/grep.ts +1 -1
  180. package/src/tools/hub/index.ts +1 -1
  181. package/src/tools/hub/launch.ts +122 -6
  182. package/src/tools/image-gen.ts +1 -1
  183. package/src/tools/index.ts +9 -0
  184. package/src/tools/inspect-image.ts +1 -1
  185. package/src/tools/learn.ts +1 -1
  186. package/src/tools/manage-skill.ts +1 -1
  187. package/src/tools/memory-edit.ts +1 -1
  188. package/src/tools/memory-recall.ts +1 -1
  189. package/src/tools/memory-reflect.ts +1 -1
  190. package/src/tools/memory-retain.ts +1 -1
  191. package/src/tools/read.ts +1 -1
  192. package/src/tools/security-scan.ts +1 -1
  193. package/src/tools/todo.ts +1 -1
  194. package/src/tools/tts.ts +1 -1
  195. package/src/tools/vibe.ts +2 -1
  196. package/src/tools/write.ts +1 -1
  197. package/src/web/search/index.ts +2 -1
  198. package/dist/types/config/file-lock.d.ts +0 -29
  199. package/dist/types/extensibility/typebox.d.ts +0 -181
  200. package/src/config/file-lock.ts +0 -164
  201. package/src/extensibility/typebox.ts +0 -958
@@ -122,12 +122,29 @@ export type DaemonRpcResult =
122
122
  export interface DaemonWireRequest {
123
123
  id: string;
124
124
  token: string;
125
+ owners?: string[];
126
+ detachedOwners?: string[];
127
+ completionEvents?: boolean;
128
+ completionAcks?: string[];
129
+ completionUnsubscribes?: string[];
130
+ completionReplays?: string[];
131
+ completionSubscriptionId?: string;
125
132
  operation: DaemonOperation;
126
133
  }
127
134
 
128
135
  /** Response envelope kept raw until matched with its pending operation. */
129
136
  export type DaemonWireResponse = { id: string; ok: true; result: unknown } | { id: string; ok: false; error: string };
130
137
 
138
+ /** Unsolicited terminal completion sent to the socket that owns a daemon. */
139
+ export interface DaemonCompletionNotification {
140
+ event: "daemon-completed";
141
+ completionId: string;
142
+ owner: string;
143
+ daemon: DaemonSnapshot;
144
+ }
145
+
146
+ export type DaemonWireMessage = DaemonWireResponse | DaemonCompletionNotification;
147
+
131
148
  function isRecord(value: unknown): value is Record<string, unknown> {
132
149
  return typeof value === "object" && value !== null && !Array.isArray(value);
133
150
  }
@@ -268,6 +285,27 @@ export function parseDaemonWireRequest(value: unknown): DaemonWireRequest {
268
285
  return {
269
286
  id: stringValue(source.id, "request.id"),
270
287
  token: stringValue(source.token, "request.token"),
288
+ owners: source.owners === undefined ? undefined : stringArray(source.owners, "request.owners"),
289
+ detachedOwners:
290
+ source.detachedOwners === undefined ? undefined : stringArray(source.detachedOwners, "request.detachedOwners"),
291
+ completionEvents:
292
+ source.completionEvents === undefined
293
+ ? undefined
294
+ : booleanValue(source.completionEvents, "request.completionEvents"),
295
+ completionAcks:
296
+ source.completionAcks === undefined ? undefined : stringArray(source.completionAcks, "request.completionAcks"),
297
+ completionUnsubscribes:
298
+ source.completionUnsubscribes === undefined
299
+ ? undefined
300
+ : stringArray(source.completionUnsubscribes, "request.completionUnsubscribes"),
301
+ completionReplays:
302
+ source.completionReplays === undefined
303
+ ? undefined
304
+ : stringArray(source.completionReplays, "request.completionReplays"),
305
+ completionSubscriptionId:
306
+ source.completionSubscriptionId === undefined
307
+ ? undefined
308
+ : stringValue(source.completionSubscriptionId, "request.completionSubscriptionId"),
271
309
  operation: parseDaemonOperation(source.operation),
272
310
  };
273
311
  }
@@ -281,6 +319,20 @@ export function parseDaemonWireResponse(value: unknown): DaemonWireResponse {
281
319
  throw new Error("response.ok must be a boolean");
282
320
  }
283
321
 
322
+ /** Decode one broker response or unsolicited completion notification. */
323
+ export function parseDaemonWireMessage(value: unknown): DaemonWireMessage {
324
+ const source = record(value, "daemon message");
325
+ if (source.event === "daemon-completed") {
326
+ return {
327
+ event: "daemon-completed",
328
+ completionId: stringValue(source.completionId, "completion.id"),
329
+ owner: stringValue(source.owner, "completion.owner"),
330
+ daemon: parseDaemonSnapshot(source.daemon),
331
+ };
332
+ }
333
+ return parseDaemonWireResponse(value);
334
+ }
335
+
284
336
  function parseDaemonOperation(value: unknown): DaemonOperation {
285
337
  const source = record(value, "daemon operation");
286
338
  const op = stringValue(source.op, "operation.op");
package/src/lsp/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type } from "arktype";
1
+ import { type } from "@oh-my-pi/omptype";
2
2
  import { TOOL_TIMEOUTS } from "../tools/tool-timeouts";
3
3
 
4
4
  // =============================================================================
package/src/main.ts CHANGED
@@ -1597,6 +1597,18 @@ export async function runRootCommand(
1597
1597
  stdoutIsTTY: process.stdout.isTTY,
1598
1598
  });
1599
1599
 
1600
+ // Startup changelog is only consumed by interactive mode below; kick the
1601
+ // CHANGELOG.md parse off now so it overlaps session creation instead of
1602
+ // serializing after it.
1603
+ const startupChangelogPromise = isInteractive
1604
+ ? logger.time(
1605
+ "main:getChangelogForDisplay",
1606
+ getChangelogForDisplay,
1607
+ parsedArgs,
1608
+ settingsInstance.get("startup.changelogMode"),
1609
+ )
1610
+ : undefined;
1611
+
1600
1612
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
1601
1613
  ...sessionOptions,
1602
1614
  eventBus,
@@ -1656,12 +1668,7 @@ export async function runRootCommand(
1656
1668
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
1657
1669
  } else if (isInteractive) {
1658
1670
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1659
- const startupChangelog = await logger.time(
1660
- "main:getChangelogForDisplay",
1661
- getChangelogForDisplay,
1662
- parsedArgs,
1663
- settingsInstance.get("startup.changelogMode"),
1664
- );
1671
+ const startupChangelog = await startupChangelogPromise;
1665
1672
 
1666
1673
  const modelScopeNotification = buildModelScopeNotification(
1667
1674
  scopedModels,
@@ -7,8 +7,8 @@ import { randomUUID } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
10
11
  import { invalidate as invalidateFsCache } from "../capability/fs";
11
- import { withFileLock } from "../config/file-lock";
12
12
 
13
13
  import { validateServerConfig } from "./config";
14
14
  import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
@@ -3,16 +3,37 @@ import { replaceTabs } from "../../tools/render-utils";
3
3
  import { getMarkdownTheme, theme } from "../theme/theme";
4
4
  import { DynamicBorder } from "./dynamic-border";
5
5
 
6
- type BtwPanelState = "running" | "complete" | "aborted" | "error";
6
+ type BtwPanelState = "running" | "complete" | "branching" | "aborted" | "error";
7
7
 
8
8
  interface BtwPanelComponentOptions {
9
9
  question: string;
10
10
  tui: TUI;
11
+ canBranch?: () => boolean;
12
+ }
13
+
14
+ class BtwFooter implements Component {
15
+ #getLine: () => string;
16
+ #line: string | undefined;
17
+ #text: Text | undefined;
18
+
19
+ constructor(getLine: () => string) {
20
+ this.#getLine = getLine;
21
+ }
22
+
23
+ render(width: number): readonly string[] {
24
+ const line = this.#getLine();
25
+ if (line !== this.#line || !this.#text) {
26
+ this.#line = line;
27
+ this.#text = new Text(line, 1, 0);
28
+ }
29
+ return this.#text.render(width);
30
+ }
11
31
  }
12
32
 
13
33
  export class BtwPanelComponent extends Container {
14
34
  #question: string;
15
35
  #tui: TUI;
36
+ #canBranch: (() => boolean) | undefined;
16
37
  #state: BtwPanelState = "running";
17
38
  #answer = "";
18
39
  #errorMessage: string | undefined;
@@ -23,6 +44,7 @@ export class BtwPanelComponent extends Container {
23
44
  super();
24
45
  this.#question = options.question;
25
46
  this.#tui = options.tui;
47
+ this.#canBranch = options.canBranch;
26
48
  this.#rebuild();
27
49
  }
28
50
 
@@ -47,6 +69,14 @@ export class BtwPanelComponent extends Container {
47
69
  this.#rebuild();
48
70
  }
49
71
 
72
+ /** Shows that the completed answer is being promoted into the chat session. */
73
+ markBranching(): void {
74
+ if (this.#closed) return;
75
+ this.#state = "branching";
76
+ this.#errorMessage = undefined;
77
+ this.#rebuild();
78
+ }
79
+
50
80
  markAborted(): void {
51
81
  if (this.#closed) return;
52
82
  this.#state = "aborted";
@@ -86,7 +116,7 @@ export class BtwPanelComponent extends Container {
86
116
  this.addChild(new Spacer(1));
87
117
  this.addChild(this.#contentComponent());
88
118
  this.addChild(new Spacer(1));
89
- this.addChild(new Text(this.#footerLine(), 1, 0));
119
+ this.addChild(new BtwFooter(() => this.#footerLine()));
90
120
  this.addChild(new Spacer(1));
91
121
  this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
92
122
  // Component-scoped: a rebuild replaces only this panel's own children
@@ -100,8 +130,15 @@ export class BtwPanelComponent extends Container {
100
130
  switch (this.#state) {
101
131
  case "running":
102
132
  return theme.fg("muted", "Esc cancel /btw");
103
- case "complete":
104
- return theme.fg("muted", this.isCopyable() ? "c copy · b branch to chat · Esc dismiss" : "Esc dismiss");
133
+ case "complete": {
134
+ if (!this.isCopyable()) return theme.fg("muted", "Esc dismiss");
135
+ const actions = ["c copy"];
136
+ if (this.#canBranch?.() ?? this.isBranchable()) actions.push("b branch to chat");
137
+ actions.push("Esc dismiss");
138
+ return theme.fg("muted", actions.join(" · "));
139
+ }
140
+ case "branching":
141
+ return theme.fg("muted", `${theme.status.pending} Branching to chat…`);
105
142
  case "aborted":
106
143
  return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
107
144
  case "error":
@@ -10,6 +10,7 @@ interface BtwRequest {
10
10
  abortController: AbortController;
11
11
  question: string;
12
12
  leafId: string | null;
13
+ sessionId: string;
13
14
  }
14
15
 
15
16
  function assistantMessageWithReplyText(assistantMessage: AssistantMessage, replyText: string): AssistantMessage {
@@ -39,6 +40,7 @@ export class BtwController {
39
40
  #lastReplyText: string | undefined;
40
41
  #lastAssistantMessage: AssistantMessage | undefined;
41
42
  #lastLeafId: string | null | undefined;
43
+ #lastSessionId: string | undefined;
42
44
  #branchInFlight = false;
43
45
  #lastCopyText: string | undefined;
44
46
  #copyInFlight = false;
@@ -50,17 +52,39 @@ export class BtwController {
50
52
  }
51
53
 
52
54
  canBranch(): boolean {
55
+ return this.#branchUnavailableReason() === undefined;
56
+ }
57
+
58
+ /** Whether plain `b` is currently reserved for a completed or pending branch action. */
59
+ handlesBranchKey(): boolean {
60
+ if (this.#branchInFlight) return true;
61
+ if (this.#activeRequest?.component.isBranchable() !== true) return false;
53
62
  return (
54
- !this.#branchInFlight &&
55
- this.#activeRequest?.component.isBranchable() === true &&
56
63
  this.#lastQuestion !== undefined &&
57
64
  this.#lastReplyText !== undefined &&
58
65
  this.#lastAssistantMessage !== undefined &&
59
- this.#lastLeafId !== null &&
60
- this.#lastLeafId === this.ctx.sessionManager.getLeafId()
66
+ this.#lastLeafId !== undefined &&
67
+ this.#lastSessionId !== undefined
61
68
  );
62
69
  }
63
70
 
71
+ #branchUnavailableReason(): string | undefined {
72
+ if (this.#branchInFlight) return "a branch is already in progress";
73
+ if (this.#activeRequest?.component.isBranchable() !== true) return "the answer is not ready";
74
+ if (!this.#lastQuestion || !this.#lastReplyText || !this.#lastAssistantMessage) {
75
+ return "the answer is unavailable";
76
+ }
77
+ if (!this.#lastLeafId) return "the session has no branch point";
78
+ if (
79
+ this.#lastSessionId !== this.ctx.sessionManager.getSessionId() ||
80
+ this.#lastLeafId !== this.ctx.sessionManager.getLeafId()
81
+ ) {
82
+ return "the session changed since /btw started";
83
+ }
84
+ if (this.ctx.session.isStreaming) return "a turn is still running";
85
+ return undefined;
86
+ }
87
+
64
88
  canCopy(): boolean {
65
89
  return (
66
90
  !this.#copyInFlight && this.#activeRequest?.component.isCopyable() === true && this.#lastCopyText !== undefined
@@ -83,17 +107,34 @@ export class BtwController {
83
107
  }
84
108
 
85
109
  async handleBranch(): Promise<boolean> {
86
- if (!this.canBranch() || !this.#lastQuestion || !this.#lastAssistantMessage) return false;
110
+ const unavailableReason = this.#branchUnavailableReason();
111
+ if (unavailableReason) {
112
+ this.ctx.showStatus(`/btw branch unavailable: ${unavailableReason}`, { dim: true });
113
+ return false;
114
+ }
115
+ const request = this.#activeRequest;
116
+ const question = this.#lastQuestion;
117
+ const assistantMessage = this.#lastAssistantMessage;
118
+ const leafId = this.#lastLeafId;
119
+ const sessionId = this.#lastSessionId;
120
+ if (!request || !question || !assistantMessage || !leafId || !sessionId) return false;
121
+
87
122
  this.#branchInFlight = true;
123
+ request.component.markBranching();
88
124
  try {
89
- await this.ctx.handleBtwBranch(this.#lastQuestion, this.#lastAssistantMessage);
125
+ await this.ctx.handleBtwBranch(question, assistantMessage, leafId, sessionId);
90
126
  return true;
91
127
  } finally {
92
128
  this.#branchInFlight = false;
129
+ if (this.#activeRequest === request) request.component.markComplete();
93
130
  }
94
131
  }
95
132
 
96
133
  handleEscape(): boolean {
134
+ if (this.#branchInFlight) {
135
+ this.ctx.showStatus("/btw branch is in progress", { dim: true });
136
+ return true;
137
+ }
97
138
  if (!this.#activeRequest) return false;
98
139
  this.#closeActiveRequest({ abort: this.#activeRequest.abortController.signal.aborted === false });
99
140
  return true;
@@ -119,10 +160,15 @@ export class BtwController {
119
160
  this.#closeActiveRequest({ abort: true });
120
161
 
121
162
  const request: BtwRequest = {
122
- component: new BtwPanelComponent({ question: trimmedQuestion, tui: this.ctx.ui }),
163
+ component: new BtwPanelComponent({
164
+ question: trimmedQuestion,
165
+ tui: this.ctx.ui,
166
+ canBranch: () => this.canBranch(),
167
+ }),
123
168
  abortController: new AbortController(),
124
169
  question: trimmedQuestion,
125
170
  leafId: this.ctx.sessionManager.getLeafId(),
171
+ sessionId: this.ctx.sessionManager.getSessionId(),
126
172
  };
127
173
  this.ctx.btwContainer.clear();
128
174
  this.ctx.btwContainer.addChild(request.component);
@@ -156,6 +202,7 @@ export class BtwController {
156
202
  this.#lastCopyText = copyText;
157
203
  this.#lastAssistantMessage = assistantMessageWithReplyText(assistantMessage, replyText);
158
204
  this.#lastLeafId = request.leafId;
205
+ this.#lastSessionId = request.sessionId;
159
206
  } else {
160
207
  this.#clearCompletedState();
161
208
  }
@@ -190,6 +237,7 @@ export class BtwController {
190
237
  this.#lastAssistantMessage = undefined;
191
238
  this.#lastCopyText = undefined;
192
239
  this.#lastLeafId = undefined;
240
+ this.#lastSessionId = undefined;
193
241
  }
194
242
 
195
243
  #isActiveRequest(request: BtwRequest): boolean {
@@ -961,6 +961,37 @@ export class CommandController {
961
961
  this.ctx.showStatus(`Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`);
962
962
  }
963
963
 
964
+ async handleResetContextCommand(): Promise<void> {
965
+ if (this.ctx.session.isCompacting) {
966
+ this.ctx.session.abortCompaction();
967
+ while (this.ctx.session.isCompacting) {
968
+ await Bun.sleep(10);
969
+ }
970
+ }
971
+ const result = await this.ctx.session.resetSessionContext();
972
+ if (!result) {
973
+ this.ctx.showWarning("Wait for the current response to finish or abort it before resetting the context.");
974
+ return;
975
+ }
976
+ // Drop the rendered transcript so the UI matches the now-empty model
977
+ // context (mirrors #runNewSessionFlow's teardown, minus the new session —
978
+ // the session id, title, and transcript file all survive).
979
+ this.ctx.clearTransientSessionUi();
980
+ this.ctx.resetTranscript();
981
+ this.ctx.statusLine.invalidate();
982
+ this.ctx.updateEditorBorderColor();
983
+ const noun = result.droppedCount === 1 ? "message" : "messages";
984
+ this.ctx.present([
985
+ new Spacer(1),
986
+ new Text(
987
+ `${theme.fg("accent", `${theme.status.success} Context reset — ${result.droppedCount} ${noun} dropped; session continues.`)}`,
988
+ 1,
989
+ 1,
990
+ ),
991
+ ]);
992
+ this.ctx.ui.requestRender(true, { clearScrollback: true });
993
+ }
994
+
964
995
  async handleDropCommand(): Promise<void> {
965
996
  if (!this.ctx.sessionManager.getSessionFile()) {
966
997
  this.ctx.showError("Nothing to drop (in-memory session)");
@@ -250,7 +250,7 @@ export class InputController {
250
250
  this.#btwBranchListenerInstalled = true;
251
251
  this.ctx.ui.addInputListener(data => {
252
252
  if (!matchesKey(data, "b")) return undefined;
253
- if (!this.ctx.canBranchBtw()) return undefined;
253
+ if (!this.ctx.handlesBtwBranchKey()) return undefined;
254
254
  if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
255
255
  if (this.ctx.editor.getText().trim()) return undefined;
256
256
  void this.ctx.handleBtwBranchKey();
@@ -801,6 +801,19 @@ export class InputController {
801
801
  this.ctx.queueCompactionMessage(text, "steer", images);
802
802
  return;
803
803
  }
804
+ // Extension commands are local actions. Execute them before the normal
805
+ // submission path creates an optimistic user message; otherwise a
806
+ // consumed command remains rendered like a prompt sent to the model.
807
+ if (this.#isLocalExtensionCommand(text)) {
808
+ this.ctx.editor.clearDraft(text);
809
+ try {
810
+ await this.ctx.session.prompt(text, { images: inputImages });
811
+ } catch (error) {
812
+ this.ctx.editor.setText(text);
813
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
814
+ }
815
+ return;
816
+ }
804
817
 
805
818
  // If streaming, use prompt() with steer behavior
806
819
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
@@ -914,14 +927,18 @@ export class InputController {
914
927
  * Local extension commands are consumed before reaching the shared session
915
928
  * title gate and must not name the conversation.
916
929
  */
917
- #maybeStartTitleGeneration(text: string): void {
918
- const runner = this.ctx.session.extensionRunner;
930
+ #isLocalExtensionCommand(text: string): boolean {
919
931
  const extensionCommandSpace = text.indexOf(" ");
920
- const isLocalExtensionCommand =
932
+ return (
921
933
  text.startsWith("/") &&
922
- runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
923
- undefined;
924
- if (isLocalExtensionCommand) {
934
+ this.ctx.session.extensionRunner?.getCommand(
935
+ extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace),
936
+ ) !== undefined
937
+ );
938
+ }
939
+
940
+ #maybeStartTitleGeneration(text: string): void {
941
+ if (this.#isLocalExtensionCommand(text)) {
925
942
  return;
926
943
  }
927
944
  this.ctx.session.maybeStartTitleGeneration(text, () => {
@@ -4513,6 +4513,10 @@ export class InteractiveMode implements InteractiveModeContext {
4513
4513
  return this.#commandController.handleFreshCommand();
4514
4514
  }
4515
4515
 
4516
+ handleResetContextCommand(): Promise<void> {
4517
+ return this.#commandController.handleResetContextCommand();
4518
+ }
4519
+
4516
4520
  async handleDropCommand(): Promise<void> {
4517
4521
  if (this.#vibeSessionTransitionBlocked()) return;
4518
4522
  this.#prepareSessionSwitch();
@@ -4844,6 +4848,11 @@ export class InteractiveMode implements InteractiveModeContext {
4844
4848
  return this.#btwController.canBranch();
4845
4849
  }
4846
4850
 
4851
+ /** Reserves plain `b` only after /btw has a completed branch action to handle. */
4852
+ handlesBtwBranchKey(): boolean {
4853
+ return this.#btwController.handlesBranchKey();
4854
+ }
4855
+
4847
4856
  handleBtwBranchKey(): Promise<boolean> {
4848
4857
  return this.#btwController.handleBranch();
4849
4858
  }
@@ -4856,9 +4865,14 @@ export class InteractiveMode implements InteractiveModeContext {
4856
4865
  return this.#btwController.handleCopy();
4857
4866
  }
4858
4867
 
4859
- async handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void> {
4868
+ async handleBtwBranch(
4869
+ question: string,
4870
+ assistantMessage: AssistantMessage,
4871
+ leafId: string,
4872
+ sessionId: string,
4873
+ ): Promise<void> {
4860
4874
  try {
4861
- const result = await this.session.branchFromBtw(question, assistantMessage);
4875
+ const result = await this.session.branchFromBtw(question, assistantMessage, leafId, sessionId);
4862
4876
  if (result.cancelled) {
4863
4877
  this.showStatus("/btw branch cancelled", { dim: true });
4864
4878
  return;
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { type } from "@oh-my-pi/omptype";
3
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
5
  import type { Effort } from "@oh-my-pi/pi-ai";
5
6
  import {
@@ -19,7 +20,6 @@ import type {
19
20
  TerminalAppearance,
20
21
  } from "@oh-my-pi/pi-tui";
21
22
  import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
22
- import { type } from "arktype";
23
23
  import chalk from "chalk";
24
24
  import { LRUCache } from "lru-cache/raw";
25
25
  // Embed theme JSON files at build time
@@ -1118,7 +1118,7 @@ const spinnerFramesSchema = type("unknown").narrow((value): value is SpinnerFram
1118
1118
  const themeJsonSchema = type({
1119
1119
  "$schema?": "string",
1120
1120
  name: "string",
1121
- "vars?": "Record<string, string | number>",
1121
+ "vars?": { "[string]": "string | number" },
1122
1122
  colors: themeColorsSchema,
1123
1123
  "export?": {
1124
1124
  "pageBg?": "string | number",
@@ -1127,7 +1127,7 @@ const themeJsonSchema = type({
1127
1127
  },
1128
1128
  "symbols?": {
1129
1129
  "preset?": "'unicode' | 'nerd' | 'ascii'",
1130
- "overrides?": "Record<string, string>",
1130
+ "overrides?": { "[string]": "string" },
1131
1131
  "spinnerFrames?": spinnerFramesSchema,
1132
1132
  },
1133
1133
  });
@@ -350,6 +350,7 @@ export interface InteractiveModeContext {
350
350
  handleDebugTranscriptCommand(): Promise<void>;
351
351
  handleClearCommand(): Promise<void>;
352
352
  handleFreshCommand(): Promise<void>;
353
+ handleResetContextCommand(): Promise<void>;
353
354
  handleDropCommand(): Promise<void>;
354
355
  handleForkCommand(): Promise<void>;
355
356
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
@@ -418,9 +419,15 @@ export interface InteractiveModeContext {
418
419
  handleBtwEscape(): boolean;
419
420
  handleBtwBranchKey(): Promise<boolean>;
420
421
  canBranchBtw(): boolean;
422
+ handlesBtwBranchKey(): boolean;
421
423
  canCopyBtw(): boolean;
422
424
  handleBtwCopyKey(): Promise<boolean>;
423
- handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void>;
425
+ handleBtwBranch(
426
+ question: string,
427
+ assistantMessage: AssistantMessage,
428
+ leafId: string,
429
+ sessionId: string,
430
+ ): Promise<void>;
424
431
  handleOmfgCommand(complaint: string): Promise<void>;
425
432
  hasActiveOmfg(): boolean;
426
433
  handleOmfgEscape(): boolean;
@@ -0,0 +1 @@
1
+ Supervised process {{name}} {{state}} {{#if hasExitCode}}with exit code {{exitCode}}{{else}}without an exit code{{/if}}.
package/src/sdk.ts CHANGED
@@ -1686,6 +1686,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1686
1686
  // entries capture it at fetch time and are dropped at injection if a newer
1687
1687
  // mutation (any tool) bumped it in the meantime.
1688
1688
  const fileMutationVersions = new Map<string, number>();
1689
+ const disposeCallbacks = new Set<() => void>();
1689
1690
  const activeToolNames = new Set<string>();
1690
1691
  const toolRegistry = new Map<string, Tool>();
1691
1692
  const setActiveToolNames = (names: Iterable<string>): void => {
@@ -1739,6 +1740,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1739
1740
  trackEvalExecution: (execution, abortController) =>
1740
1741
  session ? session.trackEvalExecution(execution, abortController) : execution,
1741
1742
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1743
+ isDisposed: () => session?.isDisposed ?? false,
1742
1744
  getHindsightSessionState: () => session?.getHindsightSessionState(),
1743
1745
  getMnemopiSessionState: () => session?.getMnemopiSessionState(),
1744
1746
  getAgentId: () => resolvedAgentId,
@@ -1765,6 +1767,14 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1765
1767
  recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
1766
1768
  getClientBridge: () => session?.clientBridge,
1767
1769
  queueDeferredDiagnostics: entry => session?.yieldQueue.enqueue(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, entry),
1770
+ queueLaunchCompletion: notification =>
1771
+ session?.queueLaunchCompletion(notification) ??
1772
+ Promise.reject(new Error("Session unavailable for launch completion delivery")),
1773
+ registerDisposeCallback: callback => {
1774
+ disposeCallbacks.add(callback);
1775
+ return () => disposeCallbacks.delete(callback);
1776
+ },
1777
+ registerSessionChangeCallback: callback => session?.registerSessionChangeCallback(callback),
1768
1778
  bumpFileMutationVersion: path => {
1769
1779
  const next = (fileMutationVersions.get(path) ?? 0) + 1;
1770
1780
  fileMutationVersions.set(path, next);
@@ -3307,6 +3317,9 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3307
3317
  const id = sessionManager.getSessionId?.();
3308
3318
  return id ? `${id}-advisor` : null;
3309
3319
  },
3320
+ queueLaunchCompletion: notification =>
3321
+ session?.queueLaunchCompletion(notification) ??
3322
+ Promise.reject(new Error("Session unavailable for launch completion delivery")),
3310
3323
  getAgentId: () => "advisor",
3311
3324
  // The primary's availability signals are wrong for advisors: their tool
3312
3325
  // slate is filtered separately at runtime (default read/grep/glob, no
@@ -3522,6 +3535,8 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3522
3535
  unsubscribeCredentialDisabled?.();
3523
3536
  unsubscribeMcpNotifications?.();
3524
3537
  unregisterMcpPostmortem?.();
3538
+ for (const callback of disposeCallbacks) callback();
3539
+ disposeCallbacks.clear();
3525
3540
  // Drop refs so the process-global postmortem list doesn't retain
3526
3541
  // the bridge closure past explicit dispose.
3527
3542
  unsubscribeMcpNotifications = undefined;