@oh-my-pi/pi-coding-agent 16.0.4 → 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 (270) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli.js +2027 -1396
  3. package/dist/types/advisor/advise-tool.d.ts +31 -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/args.d.ts +1 -0
  9. package/dist/types/cli/bench-cli.d.ts +6 -0
  10. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  11. package/dist/types/commands/launch.d.ts +3 -0
  12. package/dist/types/commands/ttsr.d.ts +57 -0
  13. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  15. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  16. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  17. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  18. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  19. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  20. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  21. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  22. package/dist/types/commit/changelog/generate.d.ts +12 -13
  23. package/dist/types/commit/shared-llm.d.ts +10 -37
  24. package/dist/types/config/config-file.d.ts +4 -4
  25. package/dist/types/config/keybindings.d.ts +5 -0
  26. package/dist/types/config/models-config-schema.d.ts +625 -990
  27. package/dist/types/config/models-config.d.ts +229 -217
  28. package/dist/types/config/settings-schema.d.ts +144 -25
  29. package/dist/types/edit/hashline/params.d.ts +7 -11
  30. package/dist/types/edit/index.d.ts +2 -1
  31. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  32. package/dist/types/edit/modes/patch.d.ts +15 -24
  33. package/dist/types/edit/modes/replace.d.ts +16 -17
  34. package/dist/types/eval/js/index.d.ts +1 -0
  35. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  36. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  37. package/dist/types/extensibility/extensions/runner.d.ts +5 -2
  38. package/dist/types/extensibility/extensions/types.d.ts +14 -10
  39. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  40. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  41. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  42. package/dist/types/extensibility/shared-events.d.ts +22 -1
  43. package/dist/types/extensibility/typebox.d.ts +80 -58
  44. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  45. package/dist/types/index.d.ts +2 -0
  46. package/dist/types/lsp/index.d.ts +11 -26
  47. package/dist/types/lsp/types.d.ts +12 -28
  48. package/dist/types/main.d.ts +1 -0
  49. package/dist/types/mcp/client.d.ts +8 -0
  50. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  51. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  52. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  53. package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
  54. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  55. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  58. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  59. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  60. package/dist/types/modes/theme/theme.d.ts +1 -1
  61. package/dist/types/modes/types.d.ts +3 -0
  62. package/dist/types/modes/utils/context-usage.d.ts +12 -0
  63. package/dist/types/sdk.d.ts +8 -1
  64. package/dist/types/session/agent-session.d.ts +24 -0
  65. package/dist/types/session/session-persistence.d.ts +4 -0
  66. package/dist/types/startup-splash.d.ts +12 -0
  67. package/dist/types/task/types.d.ts +47 -48
  68. package/dist/types/tools/ask.d.ts +26 -27
  69. package/dist/types/tools/ast-edit.d.ts +17 -17
  70. package/dist/types/tools/ast-grep.d.ts +12 -13
  71. package/dist/types/tools/bash.d.ts +20 -17
  72. package/dist/types/tools/browser.d.ts +46 -71
  73. package/dist/types/tools/checkpoint.d.ts +14 -15
  74. package/dist/types/tools/debug.d.ts +82 -145
  75. package/dist/types/tools/eval.d.ts +30 -40
  76. package/dist/types/tools/find.d.ts +17 -18
  77. package/dist/types/tools/gh.d.ts +49 -78
  78. package/dist/types/tools/image-gen.d.ts +20 -36
  79. package/dist/types/tools/inspect-image.d.ts +10 -11
  80. package/dist/types/tools/irc.d.ts +22 -33
  81. package/dist/types/tools/job.d.ts +11 -12
  82. package/dist/types/tools/learn.d.ts +21 -28
  83. package/dist/types/tools/manage-skill.d.ts +13 -22
  84. package/dist/types/tools/memory-edit.d.ts +15 -24
  85. package/dist/types/tools/memory-recall.d.ts +7 -8
  86. package/dist/types/tools/memory-reflect.d.ts +9 -10
  87. package/dist/types/tools/memory-retain.d.ts +13 -14
  88. package/dist/types/tools/read.d.ts +8 -8
  89. package/dist/types/tools/resolve.d.ts +11 -18
  90. package/dist/types/tools/review.d.ts +9 -15
  91. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  92. package/dist/types/tools/search.d.ts +16 -17
  93. package/dist/types/tools/ssh.d.ts +14 -15
  94. package/dist/types/tools/todo.d.ts +27 -43
  95. package/dist/types/tools/tts.d.ts +8 -9
  96. package/dist/types/tools/write.d.ts +9 -10
  97. package/dist/types/tui/code-cell.d.ts +2 -0
  98. package/dist/types/tui/index.d.ts +1 -0
  99. package/dist/types/tui/width-aware-text.d.ts +23 -0
  100. package/dist/types/utils/image-vision-fallback.d.ts +28 -0
  101. package/dist/types/utils/markit.d.ts +10 -1
  102. package/dist/types/web/search/index.d.ts +17 -28
  103. package/dist/types/web/search/providers/base.d.ts +1 -0
  104. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  105. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  106. package/dist/types/web/search/types.d.ts +32 -26
  107. package/package.json +14 -13
  108. package/scripts/omp +1 -1
  109. package/src/advisor/__tests__/advisor.test.ts +103 -1
  110. package/src/advisor/advise-tool.ts +47 -11
  111. package/src/autoresearch/tools/init-experiment.ts +13 -16
  112. package/src/autoresearch/tools/log-experiment.ts +15 -18
  113. package/src/autoresearch/tools/run-experiment.ts +3 -3
  114. package/src/autoresearch/tools/update-notes.ts +4 -4
  115. package/src/cli/args.ts +1 -0
  116. package/src/cli/bench-cli.ts +30 -7
  117. package/src/cli/flag-tables.ts +8 -0
  118. package/src/cli/ttsr-cli.ts +995 -0
  119. package/src/cli-commands.ts +1 -0
  120. package/src/cli.ts +7 -1
  121. package/src/collab/host.ts +2 -2
  122. package/src/commands/launch.ts +3 -0
  123. package/src/commands/ttsr.ts +125 -0
  124. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  125. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  126. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  127. package/src/commit/agentic/tools/git-overview.ts +4 -4
  128. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  129. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  130. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  131. package/src/commit/agentic/tools/schemas.ts +8 -20
  132. package/src/commit/agentic/tools/split-commit.ts +19 -23
  133. package/src/commit/analysis/summary.ts +7 -5
  134. package/src/commit/changelog/generate.ts +15 -11
  135. package/src/commit/shared-llm.ts +17 -24
  136. package/src/config/config-file.ts +13 -15
  137. package/src/config/keybindings.ts +6 -0
  138. package/src/config/models-config-schema.ts +206 -179
  139. package/src/config/settings-schema.ts +118 -2
  140. package/src/discovery/builtin-rules/index.ts +2 -0
  141. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  142. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  143. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  144. package/src/edit/hashline/params.ts +12 -11
  145. package/src/edit/index.ts +5 -4
  146. package/src/edit/modes/apply-patch.ts +4 -4
  147. package/src/edit/modes/patch.ts +15 -18
  148. package/src/edit/modes/replace.ts +13 -17
  149. package/src/edit/renderer.ts +0 -1
  150. package/src/eval/agent-bridge.ts +11 -13
  151. package/src/eval/completion-bridge.ts +25 -17
  152. package/src/eval/js/context-manager.ts +17 -2
  153. package/src/eval/js/index.ts +1 -1
  154. package/src/eval/py/executor.ts +2 -2
  155. package/src/eval/py/runner.py +44 -0
  156. package/src/extensibility/custom-commands/loader.ts +5 -3
  157. package/src/extensibility/custom-commands/types.ts +6 -3
  158. package/src/extensibility/custom-tools/loader.ts +4 -2
  159. package/src/extensibility/custom-tools/types.ts +8 -5
  160. package/src/extensibility/extensions/loader.ts +4 -2
  161. package/src/extensibility/extensions/runner.ts +20 -2
  162. package/src/extensibility/extensions/types.ts +22 -8
  163. package/src/extensibility/hooks/loader.ts +5 -2
  164. package/src/extensibility/hooks/types.ts +7 -4
  165. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  166. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  167. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  168. package/src/extensibility/shared-events.ts +24 -0
  169. package/src/extensibility/tool-proxy.ts +4 -1
  170. package/src/extensibility/typebox.ts +778 -251
  171. package/src/goals/guided-setup.ts +12 -3
  172. package/src/goals/tools/goal-tool.ts +6 -6
  173. package/src/index.ts +2 -0
  174. package/src/internal-urls/docs-index.generated.ts +15 -13
  175. package/src/lsp/types.ts +13 -27
  176. package/src/main.ts +29 -21
  177. package/src/mcp/client.ts +38 -13
  178. package/src/mcp/render.ts +102 -89
  179. package/src/modes/components/agent-hub.ts +11 -4
  180. package/src/modes/components/branch-summary-message.ts +1 -0
  181. package/src/modes/components/btw-panel.ts +5 -1
  182. package/src/modes/components/collab-prompt-message.ts +9 -7
  183. package/src/modes/components/compaction-summary-message.ts +1 -0
  184. package/src/modes/components/custom-editor.ts +18 -0
  185. package/src/modes/components/custom-message.ts +1 -0
  186. package/src/modes/components/footer.ts +6 -5
  187. package/src/modes/components/hook-message.ts +1 -0
  188. package/src/modes/components/read-tool-group.ts +9 -3
  189. package/src/modes/components/skill-message.ts +1 -0
  190. package/src/modes/components/status-line/component.ts +139 -15
  191. package/src/modes/components/status-line/context-thresholds.ts +0 -1
  192. package/src/modes/components/todo-reminder.ts +1 -0
  193. package/src/modes/components/tool-execution.ts +17 -10
  194. package/src/modes/components/ttsr-notification.ts +1 -0
  195. package/src/modes/components/user-message.ts +6 -6
  196. package/src/modes/controllers/btw-controller.ts +69 -1
  197. package/src/modes/controllers/event-controller.ts +2 -7
  198. package/src/modes/controllers/input-controller.ts +29 -0
  199. package/src/modes/controllers/selector-controller.ts +10 -3
  200. package/src/modes/interactive-mode.ts +42 -10
  201. package/src/modes/rpc/rpc-types.ts +1 -1
  202. package/src/modes/setup-wizard/index.ts +1 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  204. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  205. package/src/modes/theme/theme.ts +133 -143
  206. package/src/modes/types.ts +3 -0
  207. package/src/modes/utils/context-usage.ts +37 -20
  208. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  209. package/src/prompts/system/system-prompt.md +1 -0
  210. package/src/prompts/tools/image-attachment-describe-system.md +8 -0
  211. package/src/prompts/tools/image-attachment-describe.md +10 -0
  212. package/src/sdk.ts +35 -22
  213. package/src/session/agent-session.ts +715 -255
  214. package/src/session/session-history-format.ts +11 -2
  215. package/src/session/session-loader.ts +19 -32
  216. package/src/session/session-persistence.ts +27 -11
  217. package/src/session/snapcompact-inline.ts +1 -1
  218. package/src/slash-commands/builtin-registry.ts +4 -11
  219. package/src/ssh/connection-manager.ts +3 -2
  220. package/src/startup-splash.ts +19 -0
  221. package/src/task/executor.ts +12 -7
  222. package/src/task/types.ts +44 -41
  223. package/src/tool-discovery/tool-index.ts +17 -4
  224. package/src/tools/ask.ts +14 -14
  225. package/src/tools/ast-edit.ts +17 -14
  226. package/src/tools/ast-grep.ts +10 -9
  227. package/src/tools/bash.ts +15 -10
  228. package/src/tools/browser/launch.ts +13 -0
  229. package/src/tools/browser.ts +26 -32
  230. package/src/tools/checkpoint.ts +7 -7
  231. package/src/tools/debug.ts +72 -69
  232. package/src/tools/eval.ts +18 -19
  233. package/src/tools/find.ts +20 -13
  234. package/src/tools/gh.ts +29 -49
  235. package/src/tools/image-gen.ts +94 -57
  236. package/src/tools/inspect-image.ts +8 -9
  237. package/src/tools/irc.ts +12 -12
  238. package/src/tools/job.ts +6 -6
  239. package/src/tools/learn.ts +11 -14
  240. package/src/tools/manage-skill.ts +19 -23
  241. package/src/tools/memory-edit.ts +8 -8
  242. package/src/tools/memory-recall.ts +4 -4
  243. package/src/tools/memory-reflect.ts +5 -5
  244. package/src/tools/memory-retain.ts +9 -11
  245. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  246. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  247. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  248. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  249. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  250. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  251. package/src/tools/read.ts +197 -19
  252. package/src/tools/report-tool-issue.ts +6 -6
  253. package/src/tools/resolve.ts +6 -6
  254. package/src/tools/review.ts +10 -12
  255. package/src/tools/search-tool-bm25.ts +5 -5
  256. package/src/tools/search.ts +20 -29
  257. package/src/tools/ssh.ts +8 -8
  258. package/src/tools/todo.ts +16 -19
  259. package/src/tools/tts.ts +16 -15
  260. package/src/tools/write.ts +5 -5
  261. package/src/tui/code-cell.ts +44 -3
  262. package/src/tui/index.ts +1 -0
  263. package/src/tui/width-aware-text.ts +58 -0
  264. package/src/utils/image-vision-fallback.ts +197 -0
  265. package/src/utils/markit.ts +17 -2
  266. package/src/web/search/index.ts +21 -9
  267. package/src/web/search/providers/base.ts +1 -0
  268. package/src/web/search/providers/gemini.ts +56 -18
  269. package/src/web/search/providers/perplexity.ts +373 -126
  270. package/src/web/search/types.ts +28 -48
@@ -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
  }
@@ -1,6 +1,5 @@
1
1
  import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
2
- import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
3
- import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
2
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
4
3
  import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
5
4
  import { extractTextContent } from "../../commit/utils";
6
5
  import { settings } from "../../config/settings";
@@ -1107,11 +1106,7 @@ export class EventController {
1107
1106
  }
1108
1107
 
1109
1108
  #currentContextTokens(): number {
1110
- const lastAssistant = this.ctx.viewSession.agent.state.messages
1111
- .slice()
1112
- .reverse()
1113
- .find((m): m is AssistantMessage => m.role === "assistant" && m.stopReason !== "aborted");
1114
- return lastAssistant?.usage ? calculatePromptTokens(lastAssistant.usage) : 0;
1109
+ return this.ctx.viewSession.getContextUsage()?.tokens ?? 0;
1115
1110
  }
1116
1111
 
1117
1112
  sendCompletionNotification(): void {
@@ -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();
@@ -3,7 +3,7 @@ import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
3
3
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
4
4
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
5
5
  import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
6
- import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
6
+ import { Input, Loader, Spacer, setTuiTight, Text } from "@oh-my-pi/pi-tui";
7
7
  import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
8
8
  import { formatModelSelectorValue } from "../../config/model-resolver";
9
9
  import { getRoleInfo } from "../../config/model-roles";
@@ -67,7 +67,6 @@ import { TranscriptBlock } from "../components/transcript-container";
67
67
  import { TreeSelectorComponent } from "../components/tree-selector";
68
68
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
69
69
  import type { SessionObserverRegistry } from "../session-observer-registry";
70
- import { computeContextBreakdown } from "../utils/context-usage";
71
70
  import { buildCopyTargets } from "../utils/copy-targets";
72
71
 
73
72
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
@@ -325,6 +324,13 @@ export class SelectorController {
325
324
  }
326
325
  }
327
326
  break;
327
+ case "tui.tight":
328
+ setTuiTight(value as boolean);
329
+ this.ctx.ui.invalidate();
330
+ this.ctx.updateEditorTopBorder();
331
+ this.ctx.ui.requestRender();
332
+ break;
333
+
328
334
  case "theme": {
329
335
  setTheme(value as string, true).then(result => {
330
336
  this.ctx.statusLine.invalidate();
@@ -380,6 +386,7 @@ export class SelectorController {
380
386
  this.ctx.session.agent.repetitionPenalty = repetitionPenalty >= 0 ? repetitionPenalty : undefined;
381
387
  break;
382
388
  }
389
+ case "git.enabled":
383
390
  case "statusLinePreset":
384
391
  case "statusLine.preset":
385
392
  case "statusLineSeparator":
@@ -443,7 +450,7 @@ export class SelectorController {
443
450
  }
444
451
 
445
452
  showModelSelector(options?: { temporaryOnly?: boolean }): void {
446
- const currentContextTokens = computeContextBreakdown(this.ctx.session).usedTokens;
453
+ const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
447
454
  this.showSelector(done => {
448
455
  const selector = new ModelSelectorComponent(
449
456
  this.ctx.ui,
@@ -30,6 +30,7 @@ import {
30
30
  ProcessTerminal,
31
31
  Spacer,
32
32
  setTerminalTextSizing,
33
+ setTuiTight,
33
34
  TERMINAL,
34
35
  Text,
35
36
  TUI,
@@ -566,6 +567,7 @@ export class InteractiveMode implements InteractiveModeContext {
566
567
  );
567
568
  }
568
569
 
570
+ setTuiTight(settings.get("tui.tight"));
569
571
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
570
572
  this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
571
573
  // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
@@ -852,14 +854,16 @@ export class InteractiveMode implements InteractiveModeContext {
852
854
  }),
853
855
  );
854
856
  // Set up theme file watcher
855
- onThemeChange(() => {
856
- this.#clearWorkingMessageAccentCache();
857
- clearRenderCache();
858
- clearMermaidCache();
859
- this.ui.invalidate();
860
- this.updateEditorBorderColor();
861
- this.ui.requestRender();
862
- });
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
+ );
863
867
 
864
868
  // Subscribe to terminal dark/light appearance changes.
865
869
  // The terminal queries background color via OSC 11 at startup and on
@@ -2179,7 +2183,7 @@ export class InteractiveMode implements InteractiveModeContext {
2179
2183
  }
2180
2184
 
2181
2185
  #formatKeepContextLabel(contextUsage: ContextUsage | undefined): string {
2182
- if (contextUsage?.tokens == null) {
2186
+ if (!contextUsage) {
2183
2187
  return "Approve and keep context";
2184
2188
  }
2185
2189
  const tokens = formatContextTokenCount(contextUsage.tokens);
@@ -2188,7 +2192,7 @@ export class InteractiveMode implements InteractiveModeContext {
2188
2192
  }
2189
2193
 
2190
2194
  #isKeepContextDisabled(contextUsage: ContextUsage | undefined): boolean {
2191
- return contextUsage?.percent != null && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
2195
+ return contextUsage !== undefined && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
2192
2196
  }
2193
2197
 
2194
2198
  async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
@@ -3720,6 +3724,34 @@ export class InteractiveMode implements InteractiveModeContext {
3720
3724
  return this.#btwController.handleEscape();
3721
3725
  }
3722
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
+
3723
3755
  handleOmfgCommand(complaint: string): Promise<void> {
3724
3756
  return this.#omfgController.start(complaint);
3725
3757
  }
@@ -108,7 +108,7 @@ export interface RpcSessionState {
108
108
  /** For session dump / export (plain-text parity with /dump). */
109
109
  systemPrompt?: string[];
110
110
  dumpTools?: Array<{ name: string; description: string; parameters: unknown; examples?: readonly ToolExample[] }>;
111
- /** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
111
+ /** Current context window usage. */
112
112
  contextUsage?: ContextUsage;
113
113
  }
114
114
 
@@ -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
+ }