@oh-my-pi/pi-coding-agent 16.0.5 → 16.0.6

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 (223) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cli.js +1927 -1376
  3. package/dist/types/advisor/advise-tool.d.ts +22 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  9. package/dist/types/commands/ttsr.d.ts +57 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  19. package/dist/types/commit/changelog/generate.d.ts +12 -13
  20. package/dist/types/commit/shared-llm.d.ts +10 -37
  21. package/dist/types/config/config-file.d.ts +4 -4
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/models-config-schema.d.ts +625 -990
  24. package/dist/types/config/models-config.d.ts +229 -217
  25. package/dist/types/config/settings-schema.d.ts +53 -23
  26. package/dist/types/edit/hashline/params.d.ts +7 -11
  27. package/dist/types/edit/index.d.ts +2 -1
  28. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  29. package/dist/types/edit/modes/patch.d.ts +15 -24
  30. package/dist/types/edit/modes/replace.d.ts +16 -17
  31. package/dist/types/eval/js/index.d.ts +1 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  33. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  34. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  35. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  38. package/dist/types/extensibility/typebox.d.ts +80 -58
  39. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/lsp/index.d.ts +11 -26
  42. package/dist/types/lsp/types.d.ts +12 -28
  43. package/dist/types/mcp/client.d.ts +8 -0
  44. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +3 -0
  49. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  50. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  51. package/dist/types/modes/theme/theme.d.ts +1 -1
  52. package/dist/types/modes/types.d.ts +3 -0
  53. package/dist/types/sdk.d.ts +5 -0
  54. package/dist/types/session/agent-session.d.ts +4 -0
  55. package/dist/types/startup-splash.d.ts +12 -0
  56. package/dist/types/task/types.d.ts +47 -48
  57. package/dist/types/tools/ask.d.ts +26 -27
  58. package/dist/types/tools/ast-edit.d.ts +17 -17
  59. package/dist/types/tools/ast-grep.d.ts +12 -13
  60. package/dist/types/tools/bash.d.ts +20 -17
  61. package/dist/types/tools/browser.d.ts +46 -71
  62. package/dist/types/tools/checkpoint.d.ts +14 -15
  63. package/dist/types/tools/debug.d.ts +82 -145
  64. package/dist/types/tools/eval.d.ts +30 -40
  65. package/dist/types/tools/find.d.ts +17 -18
  66. package/dist/types/tools/gh.d.ts +49 -78
  67. package/dist/types/tools/image-gen.d.ts +20 -36
  68. package/dist/types/tools/inspect-image.d.ts +10 -11
  69. package/dist/types/tools/irc.d.ts +22 -33
  70. package/dist/types/tools/job.d.ts +11 -12
  71. package/dist/types/tools/learn.d.ts +21 -28
  72. package/dist/types/tools/manage-skill.d.ts +13 -22
  73. package/dist/types/tools/memory-edit.d.ts +15 -24
  74. package/dist/types/tools/memory-recall.d.ts +7 -8
  75. package/dist/types/tools/memory-reflect.d.ts +9 -10
  76. package/dist/types/tools/memory-retain.d.ts +13 -14
  77. package/dist/types/tools/read.d.ts +7 -8
  78. package/dist/types/tools/resolve.d.ts +11 -18
  79. package/dist/types/tools/review.d.ts +9 -15
  80. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  81. package/dist/types/tools/search.d.ts +16 -17
  82. package/dist/types/tools/ssh.d.ts +14 -15
  83. package/dist/types/tools/todo.d.ts +27 -43
  84. package/dist/types/tools/tts.d.ts +8 -9
  85. package/dist/types/tools/write.d.ts +9 -10
  86. package/dist/types/tui/index.d.ts +1 -0
  87. package/dist/types/tui/width-aware-text.d.ts +23 -0
  88. package/dist/types/utils/markit.d.ts +10 -1
  89. package/dist/types/web/search/index.d.ts +17 -28
  90. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  91. package/dist/types/web/search/types.d.ts +32 -26
  92. package/package.json +14 -13
  93. package/scripts/omp +1 -1
  94. package/src/advisor/__tests__/advisor.test.ts +44 -1
  95. package/src/advisor/advise-tool.ts +34 -11
  96. package/src/autoresearch/tools/init-experiment.ts +13 -16
  97. package/src/autoresearch/tools/log-experiment.ts +15 -18
  98. package/src/autoresearch/tools/run-experiment.ts +3 -3
  99. package/src/autoresearch/tools/update-notes.ts +4 -4
  100. package/src/cli/ttsr-cli.ts +995 -0
  101. package/src/cli-commands.ts +1 -0
  102. package/src/cli.ts +7 -1
  103. package/src/commands/ttsr.ts +125 -0
  104. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  105. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  106. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  107. package/src/commit/agentic/tools/git-overview.ts +4 -4
  108. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  109. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  110. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  111. package/src/commit/agentic/tools/schemas.ts +8 -20
  112. package/src/commit/agentic/tools/split-commit.ts +19 -23
  113. package/src/commit/analysis/summary.ts +7 -5
  114. package/src/commit/changelog/generate.ts +15 -11
  115. package/src/commit/shared-llm.ts +17 -24
  116. package/src/config/config-file.ts +13 -15
  117. package/src/config/keybindings.ts +6 -0
  118. package/src/config/models-config-schema.ts +206 -179
  119. package/src/config/settings-schema.ts +34 -0
  120. package/src/discovery/builtin-rules/index.ts +2 -0
  121. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  122. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  123. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  124. package/src/edit/hashline/params.ts +12 -11
  125. package/src/edit/index.ts +5 -4
  126. package/src/edit/modes/apply-patch.ts +4 -4
  127. package/src/edit/modes/patch.ts +15 -18
  128. package/src/edit/modes/replace.ts +13 -17
  129. package/src/edit/renderer.ts +0 -1
  130. package/src/eval/agent-bridge.ts +11 -13
  131. package/src/eval/completion-bridge.ts +25 -17
  132. package/src/eval/js/context-manager.ts +17 -2
  133. package/src/eval/js/index.ts +1 -1
  134. package/src/eval/py/executor.ts +2 -2
  135. package/src/extensibility/custom-commands/loader.ts +5 -3
  136. package/src/extensibility/custom-commands/types.ts +6 -3
  137. package/src/extensibility/custom-tools/loader.ts +4 -2
  138. package/src/extensibility/custom-tools/types.ts +8 -5
  139. package/src/extensibility/extensions/loader.ts +4 -2
  140. package/src/extensibility/extensions/types.ts +6 -3
  141. package/src/extensibility/hooks/loader.ts +5 -2
  142. package/src/extensibility/hooks/types.ts +7 -4
  143. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  144. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  145. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  146. package/src/extensibility/tool-proxy.ts +4 -1
  147. package/src/extensibility/typebox.ts +778 -251
  148. package/src/goals/guided-setup.ts +12 -3
  149. package/src/goals/tools/goal-tool.ts +6 -6
  150. package/src/index.ts +2 -0
  151. package/src/internal-urls/docs-index.generated.ts +11 -9
  152. package/src/lsp/types.ts +13 -27
  153. package/src/main.ts +19 -18
  154. package/src/mcp/client.ts +38 -13
  155. package/src/mcp/render.ts +102 -89
  156. package/src/modes/components/agent-hub.ts +11 -4
  157. package/src/modes/components/btw-panel.ts +5 -1
  158. package/src/modes/components/custom-editor.ts +18 -0
  159. package/src/modes/components/status-line/component.ts +8 -1
  160. package/src/modes/components/tool-execution.ts +17 -10
  161. package/src/modes/controllers/btw-controller.ts +69 -1
  162. package/src/modes/controllers/input-controller.ts +29 -0
  163. package/src/modes/interactive-mode.ts +38 -8
  164. package/src/modes/setup-wizard/index.ts +1 -0
  165. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  166. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  167. package/src/modes/theme/theme.ts +133 -143
  168. package/src/modes/types.ts +3 -0
  169. package/src/modes/utils/context-usage.ts +9 -5
  170. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/sdk.ts +21 -4
  173. package/src/session/agent-session.ts +160 -33
  174. package/src/session/session-history-format.ts +11 -2
  175. package/src/session/snapcompact-inline.ts +1 -1
  176. package/src/slash-commands/builtin-registry.ts +4 -11
  177. package/src/startup-splash.ts +19 -0
  178. package/src/task/executor.ts +11 -6
  179. package/src/task/types.ts +44 -41
  180. package/src/tool-discovery/tool-index.ts +17 -4
  181. package/src/tools/ask.ts +14 -14
  182. package/src/tools/ast-edit.ts +17 -14
  183. package/src/tools/ast-grep.ts +10 -9
  184. package/src/tools/bash.ts +15 -10
  185. package/src/tools/browser/launch.ts +13 -0
  186. package/src/tools/browser.ts +26 -32
  187. package/src/tools/checkpoint.ts +7 -7
  188. package/src/tools/debug.ts +72 -69
  189. package/src/tools/eval.ts +18 -19
  190. package/src/tools/find.ts +20 -13
  191. package/src/tools/gh.ts +29 -49
  192. package/src/tools/image-gen.ts +27 -32
  193. package/src/tools/inspect-image.ts +8 -9
  194. package/src/tools/irc.ts +12 -12
  195. package/src/tools/job.ts +6 -6
  196. package/src/tools/learn.ts +11 -14
  197. package/src/tools/manage-skill.ts +19 -23
  198. package/src/tools/memory-edit.ts +8 -8
  199. package/src/tools/memory-recall.ts +4 -4
  200. package/src/tools/memory-reflect.ts +5 -5
  201. package/src/tools/memory-retain.ts +9 -11
  202. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  203. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  204. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  205. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  206. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  207. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  208. package/src/tools/read.ts +169 -13
  209. package/src/tools/report-tool-issue.ts +6 -6
  210. package/src/tools/resolve.ts +6 -6
  211. package/src/tools/review.ts +10 -12
  212. package/src/tools/search-tool-bm25.ts +5 -5
  213. package/src/tools/search.ts +20 -29
  214. package/src/tools/ssh.ts +8 -8
  215. package/src/tools/todo.ts +16 -19
  216. package/src/tools/tts.ts +16 -15
  217. package/src/tools/write.ts +5 -5
  218. package/src/tui/index.ts +1 -0
  219. package/src/tui/width-aware-text.ts +58 -0
  220. package/src/utils/markit.ts +17 -2
  221. package/src/web/search/index.ts +9 -9
  222. package/src/web/search/providers/perplexity.ts +373 -126
  223. package/src/web/search/types.ts +28 -48
@@ -34,7 +34,7 @@ import {
34
34
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
35
35
  import { toolRenderers } from "../../tools/renderers";
36
36
  import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
37
- import { isFramedBlockComponent, renderStatusLine } from "../../tui";
37
+ import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
38
38
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
39
39
  import { renderDiff } from "./diff";
40
40
 
@@ -169,7 +169,7 @@ let toolExecutionInstanceSeq = 0;
169
169
  */
170
170
  export class ToolExecutionComponent extends Container implements NativeScrollbackLiveRegion {
171
171
  #contentBox: Box; // Used for custom tools and bash visual truncation
172
- #contentText: Text; // For built-in tools (with its own padding/bg)
172
+ #contentText: WidthAwareText; // Generic fallback (no custom/built-in renderer)
173
173
  #multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
174
174
  #imageComponents: Image[] = [];
175
175
  #imageSpacers: Spacer[] = [];
@@ -272,6 +272,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
272
272
  this.#ui = ui;
273
273
  this.#cwd = cwd;
274
274
  this.#args = args;
275
+ this.#editMode = resolveEditModeForTool(toolName, tool);
275
276
 
276
277
  // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
277
278
  // paddingY is 1 so background-tinted blocks (custom/extension tools and the
@@ -279,7 +280,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
279
280
  // strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
280
281
  // lines and keep their tight spacing — only tinted lines survive.
281
282
  this.#contentBox = new Box(0, 1);
282
- this.#contentText = new Text("", 1, 1);
283
+ this.#contentText = new WidthAwareText(contentWidth => this.#formatToolExecution(contentWidth), 1, 1);
283
284
 
284
285
  // Use Box for custom tools or built-in tools that have renderers
285
286
  const hasRenderer = toolName in toolRenderers;
@@ -289,8 +290,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
289
290
  } else {
290
291
  this.addChild(this.#contentText);
291
292
  }
292
-
293
- this.#editMode = resolveEditModeForTool(toolName, tool);
293
+ // Tool blocks are visually distinct cards (background-tinted or framed),
294
+ // so keep their horizontal padding even when the user enables tight layout.
295
+ this.setIgnoreTight(true);
294
296
 
295
297
  this.#updateDisplay();
296
298
  this.#editDiffInFlight = this.#runPreviewDiff();
@@ -922,9 +924,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
922
924
  }
923
925
  }
924
926
  } else {
925
- // Other built-in tools: use Text directly with caching
927
+ // Generic fallback (no custom/built-in renderer). WidthAwareText
928
+ // reformats at render time so output fills the actual terminal width
929
+ // instead of a fixed column cap.
926
930
  this.#contentText.setCustomBgFn(stateBgFn);
927
- this.#contentText.setText(this.#formatToolExecution());
931
+ this.#contentText.invalidate();
928
932
  }
929
933
 
930
934
  // Handle images (same for both custom and built-in)
@@ -1076,14 +1080,17 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1076
1080
  /**
1077
1081
  * Format a generic tool execution (fallback for tools without custom renderers)
1078
1082
  */
1079
- #formatToolExecution(): string {
1083
+ #formatToolExecution(contentWidth: number): string {
1080
1084
  const lines: string[] = [];
1081
1085
  const icon = this.#isPartial ? "pending" : this.#result?.isError ? "error" : "done";
1082
1086
  lines.push(renderStatusLine({ icon, title: this.#toolLabel }, theme));
1083
1087
 
1084
1088
  const argsObject = this.#args && typeof this.#args === "object" ? (this.#args as Record<string, unknown>) : null;
1085
1089
  if (!this.#expanded && argsObject && Object.keys(argsObject).length > 0) {
1086
- const preview = formatArgsInline(argsObject, 70);
1090
+ // Budget the inline preview against the render width, leaving room for
1091
+ // the ` └─ ` connector prefix instead of a fixed cap.
1092
+ const inlineBudget = Math.max(20, contentWidth - Bun.stringWidth(theme.tree.last) - 2);
1093
+ const preview = formatArgsInline(argsObject, inlineBudget);
1087
1094
  if (preview) {
1088
1095
  lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
1089
1096
  }
@@ -1143,7 +1150,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1143
1150
  const displayLines = outputLines.slice(0, maxOutputLines);
1144
1151
 
1145
1152
  for (const line of displayLines) {
1146
- lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80)));
1153
+ lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), contentWidth)));
1147
1154
  }
1148
1155
 
1149
1156
  if (outputLines.length > maxOutputLines) {
@@ -1,3 +1,4 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
1
2
  import { prompt } from "@oh-my-pi/pi-utils";
2
3
  import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
3
4
  import { BtwPanelComponent } from "../components/btw-panel";
@@ -7,10 +8,37 @@ interface BtwRequest {
7
8
  component: BtwPanelComponent;
8
9
  abortController: AbortController;
9
10
  question: string;
11
+ leafId: string | null;
12
+ }
13
+
14
+ function assistantMessageWithReplyText(assistantMessage: AssistantMessage, replyText: string): AssistantMessage {
15
+ const content: AssistantMessage["content"] = [];
16
+ let replacedText = false;
17
+ for (const part of assistantMessage.content) {
18
+ if (part.type === "thinking") {
19
+ content.push({ type: "thinking", thinking: part.thinking });
20
+ continue;
21
+ }
22
+ if (part.type === "redactedThinking") continue;
23
+ if (part.type !== "text") {
24
+ content.push(part);
25
+ continue;
26
+ }
27
+ if (replacedText) continue;
28
+ content.push({ type: "text", text: replyText });
29
+ replacedText = true;
30
+ }
31
+ if (!replacedText) content.push({ type: "text", text: replyText });
32
+ return { ...assistantMessage, content, providerPayload: undefined };
10
33
  }
11
34
 
12
35
  export class BtwController {
13
36
  #activeRequest: BtwRequest | undefined;
37
+ #lastQuestion: string | undefined;
38
+ #lastReplyText: string | undefined;
39
+ #lastAssistantMessage: AssistantMessage | undefined;
40
+ #lastLeafId: string | null | undefined;
41
+ #branchInFlight = false;
14
42
 
15
43
  constructor(private readonly ctx: InteractiveModeContext) {}
16
44
 
@@ -18,6 +46,29 @@ export class BtwController {
18
46
  return this.#activeRequest !== undefined;
19
47
  }
20
48
 
49
+ canBranch(): boolean {
50
+ return (
51
+ !this.#branchInFlight &&
52
+ this.#activeRequest?.component.isBranchable() === true &&
53
+ this.#lastQuestion !== undefined &&
54
+ this.#lastReplyText !== undefined &&
55
+ this.#lastAssistantMessage !== undefined &&
56
+ this.#lastLeafId !== null &&
57
+ this.#lastLeafId === this.ctx.sessionManager.getLeafId()
58
+ );
59
+ }
60
+
61
+ async handleBranch(): Promise<boolean> {
62
+ if (!this.canBranch() || !this.#lastQuestion || !this.#lastAssistantMessage) return false;
63
+ this.#branchInFlight = true;
64
+ try {
65
+ await this.ctx.handleBtwBranch(this.#lastQuestion, this.#lastAssistantMessage);
66
+ return true;
67
+ } finally {
68
+ this.#branchInFlight = false;
69
+ }
70
+ }
71
+
21
72
  handleEscape(): boolean {
22
73
  if (!this.#activeRequest) return false;
23
74
  this.#closeActiveRequest({ abort: this.#activeRequest.abortController.signal.aborted === false });
@@ -47,6 +98,7 @@ export class BtwController {
47
98
  component: new BtwPanelComponent({ question: trimmedQuestion, tui: this.ctx.ui }),
48
99
  abortController: new AbortController(),
49
100
  question: trimmedQuestion,
101
+ leafId: this.ctx.sessionManager.getLeafId(),
50
102
  };
51
103
  this.ctx.btwContainer.clear();
52
104
  this.ctx.btwContainer.addChild(request.component);
@@ -58,7 +110,7 @@ export class BtwController {
58
110
  async #runRequest(request: BtwRequest): Promise<void> {
59
111
  try {
60
112
  const promptText = prompt.render(btwUserPrompt, { question: request.question });
61
- const { replyText } = await this.ctx.session.runEphemeralTurn({
113
+ const { replyText, assistantMessage } = await this.ctx.session.runEphemeralTurn({
62
114
  promptText,
63
115
  onTextDelta: delta => {
64
116
  if (this.#isActiveRequest(request)) {
@@ -75,6 +127,14 @@ export class BtwController {
75
127
  request.component.setAnswer(replyText);
76
128
  }
77
129
  request.component.markComplete();
130
+ if (request.component.isBranchable()) {
131
+ this.#lastQuestion = request.question;
132
+ this.#lastReplyText = replyText;
133
+ this.#lastAssistantMessage = assistantMessageWithReplyText(assistantMessage, replyText);
134
+ this.#lastLeafId = request.leafId;
135
+ } else {
136
+ this.#clearBranchState();
137
+ }
78
138
  } catch (error) {
79
139
  if (!this.#isActiveRequest(request)) {
80
140
  return;
@@ -91,6 +151,7 @@ export class BtwController {
91
151
  const request = this.#activeRequest;
92
152
  if (!request) return;
93
153
  this.#activeRequest = undefined;
154
+ this.#clearBranchState();
94
155
  if (options.abort) {
95
156
  request.abortController.abort();
96
157
  }
@@ -99,6 +160,13 @@ export class BtwController {
99
160
  this.ctx.ui.requestRender();
100
161
  }
101
162
 
163
+ #clearBranchState(): void {
164
+ this.#lastQuestion = undefined;
165
+ this.#lastReplyText = undefined;
166
+ this.#lastAssistantMessage = undefined;
167
+ this.#lastLeafId = undefined;
168
+ }
169
+
102
170
  #isActiveRequest(request: BtwRequest): boolean {
103
171
  return this.#activeRequest === request;
104
172
  }
@@ -78,6 +78,7 @@ export class InputController {
78
78
 
79
79
  #enhancedPaste?: EnhancedPasteController;
80
80
  #focusedLeftTapListenerInstalled = false;
81
+ #btwBranchListenerInstalled = false;
81
82
  // Tap counter for the double-← gesture; reset whenever a quiet gap
82
83
  // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
83
84
  // #detectLeftDoubleTap.
@@ -143,6 +144,16 @@ export class InputController {
143
144
  return { consume: true };
144
145
  });
145
146
  }
147
+ if (!this.#btwBranchListenerInstalled) {
148
+ this.#btwBranchListenerInstalled = true;
149
+ this.ctx.ui.addInputListener(data => {
150
+ if (!matchesKey(data, "b")) return undefined;
151
+ if (!this.ctx.canBranchBtw()) return undefined;
152
+ if (this.ctx.editor.getText().trim()) return undefined;
153
+ void this.ctx.handleBtwBranchKey();
154
+ return { consume: true };
155
+ });
156
+ }
146
157
  this.ctx.editor.onEscape = () => {
147
158
  // Active context maintenance owns Esc: auto/manual compaction,
148
159
  // handoff generation, and auto-retry backoff all advertise
@@ -304,6 +315,8 @@ export class InputController {
304
315
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
305
316
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
306
317
  this.ctx.editor.onDequeue = () => this.handleDequeue();
318
+ this.ctx.editor.setActionKeys("app.retry", this.ctx.keybindings.getKeys("app.retry"));
319
+ this.ctx.editor.onRetry = () => void this.handleRetry();
307
320
  this.ctx.editor.clearCustomKeyHandlers();
308
321
  // Wire up extension shortcuts
309
322
  this.registerExtensionShortcuts();
@@ -925,6 +938,22 @@ export class InputController {
925
938
  return true;
926
939
  }
927
940
 
941
+ async handleRetry(): Promise<void> {
942
+ if (this.ctx.collabGuest) {
943
+ this.ctx.showStatus("/retry is host-only during a collab session");
944
+ return;
945
+ }
946
+ const didRetry = await this.ctx.viewSession.retry();
947
+ if (didRetry) {
948
+ this.ctx.editor.setText("");
949
+ this.ctx.pendingImages = [];
950
+ this.ctx.pendingImageLinks = [];
951
+ this.ctx.editor.imageLinks = undefined;
952
+ } else {
953
+ this.ctx.showStatus("Nothing to retry");
954
+ }
955
+ }
956
+
928
957
  /** Send editor text as a follow-up message (queued behind current stream). */
929
958
  async handleFollowUp(): Promise<void> {
930
959
  let text = this.ctx.editor.getText().trim();
@@ -854,14 +854,16 @@ export class InteractiveMode implements InteractiveModeContext {
854
854
  }),
855
855
  );
856
856
  // Set up theme file watcher
857
- onThemeChange(() => {
858
- this.#clearWorkingMessageAccentCache();
859
- clearRenderCache();
860
- clearMermaidCache();
861
- this.ui.invalidate();
862
- this.updateEditorBorderColor();
863
- this.ui.requestRender();
864
- });
857
+ this.#eventBusUnsubscribers.push(
858
+ onThemeChange(() => {
859
+ this.#clearWorkingMessageAccentCache();
860
+ clearRenderCache();
861
+ clearMermaidCache();
862
+ this.ui.invalidate();
863
+ this.updateEditorBorderColor();
864
+ this.ui.requestRender();
865
+ }),
866
+ );
865
867
 
866
868
  // Subscribe to terminal dark/light appearance changes.
867
869
  // The terminal queries background color via OSC 11 at startup and on
@@ -3722,6 +3724,34 @@ export class InteractiveMode implements InteractiveModeContext {
3722
3724
  return this.#btwController.handleEscape();
3723
3725
  }
3724
3726
 
3727
+ canBranchBtw(): boolean {
3728
+ return this.#btwController.canBranch();
3729
+ }
3730
+
3731
+ handleBtwBranchKey(): Promise<boolean> {
3732
+ return this.#btwController.handleBranch();
3733
+ }
3734
+
3735
+ async handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void> {
3736
+ try {
3737
+ const result = await this.session.branchFromBtw(question, assistantMessage);
3738
+ if (result.cancelled) {
3739
+ this.showStatus("/btw branch cancelled", { dim: true });
3740
+ return;
3741
+ }
3742
+ this.#btwController.dispose();
3743
+ this.#omfgController.dispose();
3744
+ this.chatContainer.clear();
3745
+ this.renderInitialMessages({ clearTerminalHistory: true });
3746
+ this.updateEditorBorderColor();
3747
+ this.showStatus(
3748
+ result.sessionFile ? `Branched /btw to ${path.basename(result.sessionFile)}` : "Branched /btw",
3749
+ );
3750
+ } catch (error) {
3751
+ this.showError(`Cannot branch /btw: ${error instanceof Error ? error.message : String(error)}`);
3752
+ }
3753
+ }
3754
+
3725
3755
  handleOmfgCommand(complaint: string): Promise<void> {
3726
3756
  return this.#omfgController.start(complaint);
3727
3757
  }
@@ -9,6 +9,7 @@ import { SetupWizardComponent } from "./wizard-overlay";
9
9
 
10
10
  export type { SetupScene, SetupSceneController, SetupSceneHost, SetupSceneResult } from "./scenes/types";
11
11
 
12
+ export { runStartupSplash } from "./startup-splash";
12
13
  export { CURRENT_SETUP_VERSION };
13
14
 
14
15
  export const ALL_SCENES = [
@@ -1,8 +1,16 @@
1
1
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
2
  import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
3
3
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
4
- import { Input, matchesKey, type SgrMouseEvent, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
4
+ import {
5
+ type Component,
6
+ type Focusable,
7
+ Input,
8
+ matchesKey,
9
+ type SgrMouseEvent,
10
+ wrapTextWithAnsi,
11
+ } from "@oh-my-pi/pi-tui";
5
12
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
13
+ import { copyToClipboard } from "../../../utils/clipboard";
6
14
  import { OAuthSelectorComponent } from "../../components/oauth-selector";
7
15
  import { theme } from "../../theme/theme";
8
16
  import type { SetupSceneHost, SetupTab } from "./types";
@@ -11,10 +19,52 @@ function loginUrlLink(url: string): string {
11
19
  return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
12
20
  }
13
21
 
22
+ function loginCopyHint(): string {
23
+ return theme.fg("dim", "(clipboard copy attempted; Alt+C retries)");
24
+ }
25
+
26
+ class CopyablePromptInput implements Component, Focusable {
27
+ #input: Input;
28
+ #onCopy: () => void;
29
+
30
+ constructor(input: Input, onCopy: () => void) {
31
+ this.#input = input;
32
+ this.#onCopy = onCopy;
33
+ }
34
+
35
+ get focused(): boolean {
36
+ return this.#input.focused;
37
+ }
38
+
39
+ set focused(value: boolean) {
40
+ this.#input.focused = value;
41
+ }
42
+
43
+ setUseTerminalCursor(useTerminalCursor: boolean): void {
44
+ this.#input.setUseTerminalCursor(useTerminalCursor);
45
+ }
46
+
47
+ render(width: number): readonly string[] {
48
+ return this.#input.render(width);
49
+ }
50
+
51
+ handleInput(data: string): void {
52
+ if (matchesKey(data, "alt+c")) {
53
+ this.#onCopy();
54
+ return;
55
+ }
56
+ this.#input.handleInput(data);
57
+ }
58
+
59
+ invalidate(): void {
60
+ this.#input.invalidate();
61
+ }
62
+ }
63
+
14
64
  interface PromptState {
15
65
  message: string;
16
66
  placeholder?: string;
17
- input: Input;
67
+ input: CopyablePromptInput;
18
68
  }
19
69
 
20
70
  /**
@@ -62,6 +112,10 @@ export class SignInTab implements SetupTab {
62
112
 
63
113
  handleInput(data: string): void {
64
114
  if (this.#loggingInProvider) {
115
+ if (this.#authUrl && (matchesKey(data, "alt+c") || (data === "c" && !this.#prompt))) {
116
+ void this.#copyAuthUrl();
117
+ return;
118
+ }
65
119
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
66
120
  this.#loginAbort?.abort();
67
121
  }
@@ -88,7 +142,10 @@ export class SignInTab implements SetupTab {
88
142
 
89
143
  const urlLines = this.#authUrl ? wrapTextWithAnsi(theme.fg("dim", this.#authUrl), width) : [];
90
144
  if (this.#authUrl) {
91
- lines.push(theme.fg("accent", `Browser login: ${loginUrlLink(this.#authUrl)}`), ...urlLines.slice(0, 2));
145
+ lines.push(
146
+ theme.fg("accent", `Browser login: ${loginUrlLink(this.#authUrl)} ${loginCopyHint()}`),
147
+ ...urlLines.slice(0, 2),
148
+ );
92
149
  }
93
150
  if (this.#prompt) {
94
151
  lines.push(theme.fg("warning", this.#prompt.message));
@@ -140,6 +197,7 @@ export class SignInTab implements SetupTab {
140
197
  if (useManualInput) {
141
198
  this.#statusLines.push(theme.fg("dim", "Paste the returned code or redirect URL when prompted."));
142
199
  }
200
+ void this.#copyAuthUrl();
143
201
  this.host.ctx.openInBrowser(info.url);
144
202
  this.host.requestRender();
145
203
  },
@@ -184,12 +242,26 @@ export class SignInTab implements SetupTab {
184
242
  }
185
243
  }
186
244
 
245
+ async #copyAuthUrl(): Promise<void> {
246
+ const url = this.#authUrl;
247
+ if (!url) return;
248
+ try {
249
+ await copyToClipboard(url);
250
+ } catch {
251
+ // Clipboard integration is best-effort; the full URL remains rendered below.
252
+ }
253
+ this.host.requestRender();
254
+ }
255
+
187
256
  #showPrompt(prompt: { message: string; placeholder?: string }): Promise<string> {
188
257
  this.#resolvePrompt("");
189
258
  const input = new Input();
259
+ const focusInput = new CopyablePromptInput(input, () => {
260
+ void this.#copyAuthUrl();
261
+ });
190
262
  const pending = Promise.withResolvers<string>();
191
263
  this.#promptResolve = pending.resolve;
192
- this.#prompt = { message: prompt.message, placeholder: prompt.placeholder, input };
264
+ this.#prompt = { message: prompt.message, placeholder: prompt.placeholder, input: focusInput };
193
265
  input.onSubmit = value => {
194
266
  this.#resolvePrompt(value);
195
267
  };
@@ -197,7 +269,7 @@ export class SignInTab implements SetupTab {
197
269
  this.#loginAbort?.abort();
198
270
  this.#resolvePrompt("");
199
271
  };
200
- this.host.setFocus(input);
272
+ this.host.setFocus(focusInput);
201
273
  this.host.requestRender();
202
274
  return pending.promise;
203
275
  }
@@ -0,0 +1,107 @@
1
+ import { type Component, matchesKey, type OverlayFocusOwner } from "@oh-my-pi/pi-tui";
2
+ import type { InteractiveModeContext } from "../types";
3
+ import { renderSetupSplash, SETUP_SPLASH_MS, SETUP_TICK_MS } from "./scenes/splash";
4
+
5
+ export interface RunStartupSplashOptions {
6
+ readonly durationMs?: number;
7
+ readonly tickMs?: number;
8
+ readonly now?: () => number;
9
+ }
10
+
11
+ class StartupSplashComponent implements Component, OverlayFocusOwner {
12
+ #phaseStartedAt = 0;
13
+ #timer: NodeJS.Timeout | undefined;
14
+ #done = Promise.withResolvers<void>();
15
+ #disposed = false;
16
+ readonly #durationMs: number;
17
+ readonly #tickMs: number;
18
+ readonly #now: () => number;
19
+
20
+ constructor(
21
+ readonly ctx: InteractiveModeContext,
22
+ options: RunStartupSplashOptions = {},
23
+ ) {
24
+ this.#durationMs = options.durationMs ?? SETUP_SPLASH_MS;
25
+ this.#tickMs = options.tickMs ?? SETUP_TICK_MS;
26
+ this.#now = options.now ?? (() => performance.now());
27
+ }
28
+
29
+ run(): Promise<void> {
30
+ this.#phaseStartedAt = this.#now();
31
+ this.#startTimer();
32
+ this.ctx.ui.requestRender();
33
+ return this.#done.promise;
34
+ }
35
+
36
+ dispose(): void {
37
+ this.#disposed = true;
38
+ this.#stopTimer();
39
+ }
40
+
41
+ ownsOverlayFocusTarget(component: Component): boolean {
42
+ return component === this;
43
+ }
44
+
45
+ handleInput(data: string): void {
46
+ if (
47
+ matchesKey(data, "enter") ||
48
+ matchesKey(data, "return") ||
49
+ matchesKey(data, "space") ||
50
+ matchesKey(data, "escape")
51
+ ) {
52
+ this.#complete();
53
+ }
54
+ }
55
+
56
+ render(width: number): readonly string[] {
57
+ const elapsedMs = Math.min(this.#durationMs, Math.max(0, this.#now() - this.#phaseStartedAt));
58
+ return renderSetupSplash(Math.max(1, width), Math.max(1, this.ctx.ui.terminal.rows), elapsedMs);
59
+ }
60
+
61
+ #startTimer(): void {
62
+ if (this.#timer) return;
63
+ this.#timer = setInterval(() => {
64
+ if (this.#disposed) return;
65
+ const elapsed = this.#now() - this.#phaseStartedAt;
66
+ if (elapsed >= this.#durationMs) {
67
+ this.#complete();
68
+ return;
69
+ }
70
+ this.ctx.ui.requestRender();
71
+ }, this.#tickMs);
72
+ }
73
+
74
+ #stopTimer(): void {
75
+ if (!this.#timer) return;
76
+ clearInterval(this.#timer);
77
+ this.#timer = undefined;
78
+ }
79
+
80
+ #complete(): void {
81
+ if (this.#disposed) return;
82
+ this.#stopTimer();
83
+ this.#done.resolve();
84
+ }
85
+ }
86
+
87
+ export async function runStartupSplash(
88
+ ctx: InteractiveModeContext,
89
+ options: RunStartupSplashOptions = {},
90
+ ): Promise<void> {
91
+ const component = new StartupSplashComponent(ctx, options);
92
+ const overlay = ctx.ui.showOverlay(component, {
93
+ width: "100%",
94
+ maxHeight: "100%",
95
+ anchor: "top-left",
96
+ margin: 0,
97
+ fullscreen: true,
98
+ });
99
+ try {
100
+ ctx.ui.setFocus(component);
101
+ await component.run();
102
+ } finally {
103
+ component.dispose();
104
+ ctx.ui.setFocus(component);
105
+ overlay.hide();
106
+ }
107
+ }