@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Pure heading/section parser for the plan-review overlay. It splits a plan's
3
+ * markdown into a flat list of sections — a leading preamble (text before the
4
+ * first heading) followed by one entry per ATX heading — preserving the exact
5
+ * source bytes of each section so the overlay can render, reorder-free delete,
6
+ * and round-trip the document without a full markdown re-render.
7
+ *
8
+ * No TUI dependencies: this module is unit-tested in isolation.
9
+ */
10
+
11
+ /** ATX heading: 1-6 `#`, required whitespace, a title, optional closing `#`s. */
12
+ const HEADING_RE = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
13
+ /** Opening/closing code fence run (``` or ~~~), allowing up to 3 lead spaces. */
14
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
15
+
16
+ export interface PlanSection {
17
+ /** `0` = preamble (no heading, no ToC entry); `1..6` = heading depth. */
18
+ level: number;
19
+ /** Plain-text heading label with inline markdown lightly stripped. */
20
+ title: string;
21
+ /** Exact source slice for this section, including its trailing newline(s). */
22
+ raw: string;
23
+ }
24
+
25
+ /**
26
+ * Collapse inline markdown emphasis/link/code syntax to readable text. This is
27
+ * a deliberately light strip (not a full markdown render) just so ToC entries
28
+ * read cleanly — `**Goal** & [docs](x)` becomes `Goal & docs`.
29
+ */
30
+ export function stripInlineMarkdown(text: string): string {
31
+ let out = text;
32
+ // Images first (so the link pass below does not eat the `(url)`), then links.
33
+ out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
34
+ out = out.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
35
+ out = out.replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1");
36
+ // Autolinks `<https://…>` keep their URL as the readable text.
37
+ out = out.replace(/<([^>\s]+)>/g, "$1");
38
+ // Inline code, then bold/italic/strikethrough emphasis runs.
39
+ out = out.replace(/`([^`]+)`/g, "$1");
40
+ out = out.replace(/(\*\*|__)(.+?)\1/g, "$2");
41
+ out = out.replace(/(\*|_)(.+?)\1/g, "$2");
42
+ out = out.replace(/~~(.+?)~~/g, "$1");
43
+ return out.replace(/\s+/g, " ").trim();
44
+ }
45
+
46
+ /**
47
+ * Split `text` into preamble + heading sections. `#` characters inside fenced
48
+ * code blocks are never treated as headings. Concatenating every section's
49
+ * `raw` reproduces the original text exactly.
50
+ */
51
+ export function parsePlanSections(text: string): PlanSection[] {
52
+ const lines = text.split("\n");
53
+ // Character offset of each line start so section `raw` can slice the source.
54
+ const offsets: number[] = new Array(lines.length);
55
+ let cursor = 0;
56
+ for (let i = 0; i < lines.length; i++) {
57
+ offsets[i] = cursor;
58
+ cursor += lines[i]!.length + 1; // +1 for the "\n" join separator
59
+ }
60
+
61
+ // Heading line indices (start of each heading section), with metadata.
62
+ const heads: { line: number; level: number; title: string }[] = [];
63
+ let fenceChar: string | null = null;
64
+ let fenceLen = 0;
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i]!;
67
+ const fence = FENCE_RE.exec(line);
68
+ if (fenceChar === null) {
69
+ if (fence) {
70
+ fenceChar = fence[1]![0]!;
71
+ fenceLen = fence[1]!.length;
72
+ }
73
+ // Opening-fence lines are body, not headings.
74
+ if (fence) continue;
75
+ } else {
76
+ // Inside a fence: only a matching-or-longer run of the same char closes.
77
+ if (fence && fence[1]![0] === fenceChar && fence[1]!.length >= fenceLen && fence[2]!.trim() === "") {
78
+ fenceChar = null;
79
+ fenceLen = 0;
80
+ }
81
+ continue;
82
+ }
83
+ const heading = HEADING_RE.exec(line);
84
+ if (heading) {
85
+ heads.push({ line: i, level: heading[1]!.length, title: stripInlineMarkdown(heading[2]!) });
86
+ }
87
+ }
88
+
89
+ const sections: PlanSection[] = [];
90
+ const sliceRaw = (startLine: number, endLine: number): string => {
91
+ const startOffset = offsets[startLine]!;
92
+ const endOffset = endLine < lines.length ? offsets[endLine]! : text.length;
93
+ return text.slice(startOffset, endOffset);
94
+ };
95
+
96
+ // Preamble: everything before the first heading (only when non-empty).
97
+ const firstHeadLine = heads.length > 0 ? heads[0]!.line : lines.length;
98
+ if (firstHeadLine > 0) {
99
+ const raw = sliceRaw(0, firstHeadLine);
100
+ if (raw.length > 0) sections.push({ level: 0, title: "", raw });
101
+ }
102
+
103
+ for (let h = 0; h < heads.length; h++) {
104
+ const head = heads[h]!;
105
+ const endLine = h + 1 < heads.length ? heads[h + 1]!.line : lines.length;
106
+ sections.push({ level: head.level, title: head.title, raw: sliceRaw(head.line, endLine) });
107
+ }
108
+
109
+ return sections;
110
+ }
111
+
112
+ /**
113
+ * Concatenate every section's `raw` back into a single document and guarantee a
114
+ * single trailing newline. Inverse of {@link parsePlanSections} for any input
115
+ * that already ends with a newline.
116
+ */
117
+ export function joinPlanSections(sections: readonly PlanSection[]): string {
118
+ let joined = "";
119
+ for (const section of sections) joined += section.raw;
120
+ if (joined.length === 0) return "";
121
+ return joined.endsWith("\n") ? joined : `${joined}\n`;
122
+ }
123
+
124
+ /**
125
+ * Indices to remove when deleting `sections[index]`: the heading itself plus
126
+ * every following section nested deeper than it (its sub-headings). The
127
+ * preamble (level 0) is never a deletion target and yields an empty span.
128
+ */
129
+ export function sectionDeletionSpan(sections: readonly PlanSection[], index: number): number[] {
130
+ const target = sections[index];
131
+ if (!target || target.level === 0) return [];
132
+ const span = [index];
133
+ for (let j = index + 1; j < sections.length; j++) {
134
+ if (sections[j]!.level > target.level) span.push(j);
135
+ else break;
136
+ }
137
+ return span;
138
+ }
@@ -81,6 +81,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
81
81
  #text: Text;
82
82
  #expanded = false;
83
83
  #showContentPreview: boolean;
84
+ // A read group accretes entries across multiple assistant completions for as
85
+ // long as the run of reads is uninterrupted. While it is the active group it
86
+ // must stay in the transcript's repaintable live region — its header line
87
+ // re-layouts from `Read <path>` to `Read (N)` + tree as entries arrive, so a
88
+ // frozen snapshot taken on a risk terminal would strand the single-entry form
89
+ // (see TranscriptContainer / NativeScrollbackLiveRegion). The controller calls
90
+ // `finalize()` once the run breaks so the block can commit to native scrollback.
91
+ #finalized = false;
84
92
 
85
93
  constructor(options: ReadToolGroupOptions = {}) {
86
94
  super();
@@ -90,6 +98,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
90
98
  this.#updateDisplay();
91
99
  }
92
100
 
101
+ isTranscriptBlockFinalized(): boolean {
102
+ return this.#finalized;
103
+ }
104
+
105
+ finalize(): void {
106
+ this.#finalized = true;
107
+ }
108
+
93
109
  updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
94
110
  if (!toolCallId) return;
95
111
  const basePath = args.file_path || args.path || "";
@@ -181,9 +197,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
181
197
  const total = entriesWithoutPreview.length;
182
198
  for (const [index, entry] of entriesWithoutPreview.entries()) {
183
199
  const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
184
- const statusSymbol = this.#formatStatus(entry.status);
200
+ const statusPrefix = entry.status === "success" ? "" : `${this.#formatStatus(entry.status)} `;
185
201
  const pathDisplay = this.#formatPath(entry);
186
- lines.push(` ${theme.fg("dim", connector)} ${statusSymbol} ${pathDisplay}`.trimEnd());
202
+ lines.push(` ${theme.fg("dim", connector)} ${statusPrefix}${pathDisplay}`.trimEnd());
187
203
  }
188
204
 
189
205
  this.#text.setText(lines.join("\n"));
@@ -198,7 +214,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
198
214
 
199
215
  /**
200
216
  * Add a code-cell content preview below the entry summary.
201
- * When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with "… N more lines (Ctrl+O for more)" hint.
217
+ * When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with a "… N more lines ⟨<key>: Expand⟩" hint.
202
218
  * When expanded: shows full content.
203
219
  */
204
220
  #addContentPreview(entry: ReadEntry): void {
@@ -254,7 +270,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
254
270
 
255
271
  #formatStatus(status: ReadEntry["status"]): string {
256
272
  if (status === "success") {
257
- return theme.fg("success", theme.status.success);
273
+ return theme.fg("text", theme.status.enabled);
258
274
  }
259
275
  if (status === "warning") {
260
276
  return theme.fg("warning", theme.status.warning);
@@ -11,7 +11,6 @@ export class SkillMessageComponent extends Container {
11
11
 
12
12
  constructor(private readonly message: CustomMessage<SkillPromptDetails>) {
13
13
  super();
14
- this.addChild(new Spacer(1));
15
14
 
16
15
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
17
16
  this.#rebuild();
@@ -40,6 +40,11 @@ export interface StatusLineSettings {
40
40
  sessionAccent?: boolean;
41
41
  }
42
42
 
43
+ export type EffectiveStatusLineSettings = Required<
44
+ Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
45
+ > &
46
+ StatusLineSettings;
47
+
43
48
  // ═══════════════════════════════════════════════════════════════════════════
44
49
  // Per-message token cache
45
50
  // ═══════════════════════════════════════════════════════════════════════════
@@ -143,6 +148,7 @@ function tokensForMessage(msg: AgentMessage): number {
143
148
 
144
149
  export class StatusLineComponent implements Component {
145
150
  #settings: StatusLineSettings = {};
151
+ #effectiveSettings: EffectiveStatusLineSettings | undefined;
146
152
  #cachedBranch: string | null | undefined = undefined;
147
153
  #cachedBranchRepoId: string | null | undefined = undefined;
148
154
  #gitWatcher: fs.FSWatcher | null = null;
@@ -204,6 +210,11 @@ export class StatusLineComponent implements Component {
204
210
 
205
211
  updateSettings(settings: StatusLineSettings): void {
206
212
  this.#settings = settings;
213
+ this.#effectiveSettings = undefined;
214
+ }
215
+
216
+ getEffectiveSettingsForTest(): EffectiveStatusLineSettings {
217
+ return this.#resolveSettings();
207
218
  }
208
219
 
209
220
  setAutoCompactEnabled(enabled: boolean): void {
@@ -594,10 +605,14 @@ export class StatusLineComponent implements Component {
594
605
  };
595
606
  }
596
607
 
597
- #resolveSettings(): Required<
598
- Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
599
- > &
600
- StatusLineSettings {
608
+ #resolveSettings(): EffectiveStatusLineSettings {
609
+ if (this.#effectiveSettings === undefined) {
610
+ this.#effectiveSettings = this.#computeEffectiveSettings();
611
+ }
612
+ return this.#effectiveSettings;
613
+ }
614
+
615
+ #computeEffectiveSettings(): EffectiveStatusLineSettings {
601
616
  const preset = this.#settings.preset ?? "default";
602
617
  const presetDef = getPreset(preset);
603
618
  const useCustomSegments = preset === "custom";
@@ -1,5 +1,6 @@
1
1
  Tired of typing "keep going"? Just send a '.'
2
2
  You can /btw to ask a side question
3
+ Use /tan to fork the current conversation into a background agent
3
4
  Ctrl+D can be used to exit, but with your draft saved!
4
5
  Find out which model you emotionally abuse the most with `omp stats`
5
6
  Try task isolation to create CoW worktrees
@@ -8,7 +9,7 @@ Spaghetti code? Try complaining with /omfg
8
9
  Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
9
10
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
10
11
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
11
- Say `workflow` in your message to drive the task with parallel subagents in eval — watch it glow as you type
12
+ Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
12
13
  Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
13
14
  Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
14
15
  Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
@@ -16,8 +16,6 @@ export class TodoReminderComponent extends Container {
16
16
  ) {
17
17
  super();
18
18
 
19
- this.addChild(new Spacer(1));
20
-
21
19
  this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
22
20
  this.addChild(this.#box);
23
21
 
@@ -37,26 +37,6 @@ import { isFramedBlockComponent, renderStatusLine } from "../../tui";
37
37
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
38
38
  import { renderDiff } from "./diff";
39
39
 
40
- function ensureInvalidate(component: unknown): Component {
41
- const c = component as { render: Component["render"]; invalidate?: () => void };
42
- if (!c.invalidate) {
43
- c.invalidate = () => {};
44
- }
45
- return c as Component;
46
- }
47
-
48
- function addBoxChild(box: Box, component: unknown): boolean {
49
- const child = ensureInvalidate(component);
50
- box.addChild(child);
51
- return isFramedBlockComponent(child);
52
- }
53
-
54
- function setBoxPaddingForFramedBlock(box: Box, hasFramedBlock: boolean): void {
55
- const padding = hasFramedBlock ? 0 : 1;
56
- box.setPaddingX(padding);
57
- box.setPaddingY(padding);
58
- }
59
-
60
40
  /**
61
41
  * Drop trailing removal/hunk-header lines that appear in a streaming diff
62
42
  * before the matching `+added` lines have arrived. Without this, a partial
@@ -153,12 +133,12 @@ export interface ToolExecutionHandle {
153
133
  setExpanded(expanded: boolean): void;
154
134
  }
155
135
 
156
- /** Drive pending-tool redraws at ~60fps so the animated border sweep is smooth.
157
- * The TUI already throttles at its 16ms `MIN_RENDER_INTERVAL_MS`, so this is the
158
- * natural upper bound and static frames diff to a no-op redraw at ~zero cost. */
159
- const SPINNER_RENDER_INTERVAL_MS = 16;
136
+ /** Drive pending-tool redraws at 30fps so the animated border sweep stays
137
+ * smooth without spending twice the frame budget. The TUI throttles at the same
138
+ * cadence, and static frames diff to a no-op redraw at ~zero cost. */
139
+ const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
160
140
  /** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
161
- * 60fps render cadence (mirrors `Loader`). */
141
+ * render cadence (mirrors `Loader`). */
162
142
  const SPINNER_GLYPH_ADVANCE_MS = 80;
163
143
 
164
144
  // Stable per-instance counter so each tool execution's inline images get a
@@ -245,11 +225,13 @@ export class ToolExecutionComponent extends Container {
245
225
  this.#cwd = cwd;
246
226
  this.#args = args;
247
227
 
248
- this.addChild(new Spacer(1));
249
-
250
- // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
251
- this.#contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
252
- this.#contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
228
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
229
+ // paddingY is 1 so background-tinted blocks (custom/extension tools and the
230
+ // generic fallback) get top/bottom breathing room. TranscriptContainer
231
+ // strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
232
+ // lines and keep their tight spacing only tinted lines survive.
233
+ this.#contentBox = new Box(0, 1);
234
+ this.#contentText = new Text("", 1, 1);
253
235
 
254
236
  // Use Box for custom tools or built-in tools that have renderers
255
237
  const hasRenderer = toolName in toolRenderers;
@@ -439,26 +421,43 @@ export class ToolExecutionComponent extends Container {
439
421
  #updateSpinnerAnimation(): void {
440
422
  // Spinner for: task tool with partial result, or edit/write while args streaming
441
423
  const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
442
- const isBackgroundAsyncTask =
443
- this.#toolName === "task" &&
424
+ const isBackgroundAsyncRunning =
444
425
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
426
+ const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
445
427
  const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
446
- // Sweep the border of bash/eval execution blocks while they're pending.
428
+ // Sweep the border of bash/eval execution blocks while they're pending — but
429
+ // not once they've been backgrounded: a backgrounded job's block gets
430
+ // committed to scrollback and finalizes later via the async update path, so a
431
+ // mid-sweep frame would freeze a stray dark "bar" segment into the border.
447
432
  const isPendingExecBlock =
448
- this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
433
+ this.#isPartial &&
434
+ shimmerEnabled() &&
435
+ (this.#toolName === "bash" || this.#toolName === "eval") &&
436
+ !isBackgroundAsyncRunning;
449
437
  const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
450
438
  if (needsSpinner && !this.#spinnerInterval) {
451
- this.#lastSpinnerAdvanceAt = performance.now();
439
+ const now = performance.now();
440
+ const frameCount = theme.spinnerFrames.length;
441
+ this.#lastSpinnerAdvanceAt = now;
442
+ if (frameCount > 0 && this.#spinnerFrame === undefined) {
443
+ this.#spinnerFrame = 0;
444
+ this.#renderState.spinnerFrame = 0;
445
+ }
452
446
  this.#spinnerInterval = setInterval(() => {
453
447
  const now = performance.now();
454
448
  const frameCount = theme.spinnerFrames.length;
455
- // Redraw at ~60fps for a smooth border sweep, but only step the spinner
456
- // glyph at its classic ~12.5fps cadence. The TUI throttles renders at
457
- // 16ms and the differ drops no-op redraws, so the extra ticks are free.
458
- if (frameCount > 0 && now - this.#lastSpinnerAdvanceAt >= SPINNER_GLYPH_ADVANCE_MS) {
459
- this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
460
- this.#renderState.spinnerFrame = this.#spinnerFrame;
461
- this.#lastSpinnerAdvanceAt = now;
449
+ // Redraw at 30fps for a smooth border sweep, but keep the spinner
450
+ // glyph phase-locked to its classic ~12.5fps cadence. Advancing the
451
+ // anchor by elapsed frames instead of resetting to `now` avoids the
452
+ // 30fps timer quantizing the glyph down to one step every three ticks.
453
+ if (frameCount > 0) {
454
+ const elapsed = now - this.#lastSpinnerAdvanceAt;
455
+ if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
456
+ const steps = Math.floor(elapsed / SPINNER_GLYPH_ADVANCE_MS);
457
+ this.#spinnerFrame = ((this.#spinnerFrame ?? 0) + steps) % frameCount;
458
+ this.#renderState.spinnerFrame = this.#spinnerFrame;
459
+ this.#lastSpinnerAdvanceAt += steps * SPINNER_GLYPH_ADVANCE_MS;
460
+ }
462
461
  }
463
462
  this.#ui.requestRender();
464
463
  }, SPINNER_RENDER_INTERVAL_MS);
@@ -607,27 +606,24 @@ export class ToolExecutionComponent extends Container {
607
606
  }
608
607
 
609
608
  #updateDisplay(): void {
610
- // Set background based on state
611
- const bgFn = this.#isPartial
612
- ? (text: string) => theme.bg("toolPendingBg", text)
613
- : this.#result?.isError
614
- ? (text: string) => theme.bg("toolErrorBg", text)
615
- : (text: string) => theme.bg("toolSuccessBg", text);
616
-
617
609
  // Sync shared mutable render state for component closures
618
610
  this.#renderState.expanded = this.#expanded;
619
611
  this.#renderState.isPartial = this.#isPartial;
620
612
  this.#renderState.spinnerFrame = this.#spinnerFrame;
621
613
 
614
+ // Non-self-framing tools (custom/extension renderers and the generic
615
+ // fallback) get a padded, state-tinted block — built-ins that draw their
616
+ // own frame opt out below via the framed-component mark.
617
+ const stateBgKey = this.#isPartial ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
618
+ const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
619
+
622
620
  // Check for custom tool rendering
623
621
  if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
624
622
  const tool = this.#tool;
625
623
  const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
626
624
  // Custom tools use Box for flexible component rendering
627
- const inline = Boolean((tool as { inline?: boolean }).inline);
628
- this.#contentBox.setBgFn(inline ? undefined : bgFn);
625
+ this.#contentBox.setBgFn(undefined);
629
626
  this.#contentBox.clear();
630
- let contentBoxHasFramedBlock = false;
631
627
  // Mirror the built-in renderer branch so custom renderers (notably the
632
628
  // task tool, whose live instance routes through here) receive the same
633
629
  // render context — e.g. the `hasResult` flag that suppresses the task
@@ -643,18 +639,15 @@ export class ToolExecutionComponent extends Container {
643
639
  if (tool.renderCall) {
644
640
  try {
645
641
  const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
646
- if (callComponent) {
647
- contentBoxHasFramedBlock =
648
- addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
649
- }
642
+ if (callComponent) this.#contentBox.addChild(callComponent as Component);
650
643
  } catch (err) {
651
644
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
652
645
  // Fall back to default on error
653
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
646
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
654
647
  }
655
648
  } else {
656
649
  // No custom renderCall, show tool name
657
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
650
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
658
651
  }
659
652
  }
660
653
 
@@ -677,25 +670,27 @@ export class ToolExecutionComponent extends Container {
677
670
  theme,
678
671
  this.#args,
679
672
  );
680
- if (resultComponent) {
681
- contentBoxHasFramedBlock = addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
682
- }
673
+ if (resultComponent) this.#contentBox.addChild(resultComponent);
683
674
  } catch (err) {
684
675
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
685
676
  // Fall back to showing raw output on error
686
677
  const output = this.#getTextOutput();
687
678
  if (output) {
688
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
679
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
689
680
  }
690
681
  }
691
682
  } else if (this.#result) {
692
683
  // Has result but no custom renderResult
693
684
  const output = this.#getTextOutput();
694
685
  if (output) {
695
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
686
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
696
687
  }
697
688
  }
698
- setBoxPaddingForFramedBlock(this.#contentBox, contentBoxHasFramedBlock);
689
+ // Custom tools that draw their own frame (task) render flush; plain
690
+ // extension renderers get the padded, state-tinted block back.
691
+ const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
692
+ this.#contentBox.setPaddingX(customFramed ? 0 : 1);
693
+ this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
699
694
  } else if (this.#toolName in toolRenderers) {
700
695
  // Built-in tools with renderers
701
696
  const renderer = toolRenderers[this.#toolName];
@@ -714,7 +709,6 @@ export class ToolExecutionComponent extends Container {
714
709
  // Multi-file: render each file as its own Box (identical to separate tool calls)
715
710
  this.#contentBox.setBgFn(undefined);
716
711
  this.#contentBox.clear();
717
- this.#contentBox.setPaddingX(1);
718
712
 
719
713
  const renderContext = this.#buildRenderContext();
720
714
  this.#renderState.renderContext = renderContext;
@@ -726,20 +720,14 @@ export class ToolExecutionComponent extends Container {
726
720
  this.#multiFileBoxes.push(spacer);
727
721
  this.addChild(spacer);
728
722
  }
729
- const fileBgFn = fileResult.isError
730
- ? (text: string) => theme.bg("toolErrorBg", text)
731
- : (text: string) => theme.bg("toolSuccessBg", text);
732
- const fileBox = new Box(1, 1, fileBgFn);
723
+ const fileBox = new Box(0, 0);
733
724
  try {
734
725
  const resultComponent = renderer.renderResult(
735
726
  { content: [], details: fileResult, isError: fileResult.isError },
736
727
  this.#renderState,
737
728
  theme,
738
729
  );
739
- if (resultComponent) {
740
- const fileBoxHasFramedBlock = addBoxChild(fileBox, resultComponent);
741
- setBoxPaddingForFramedBlock(fileBox, fileBoxHasFramedBlock);
742
- }
730
+ if (resultComponent) fileBox.addChild(resultComponent);
743
731
  } catch (err) {
744
732
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
745
733
  }
@@ -756,7 +744,7 @@ export class ToolExecutionComponent extends Container {
756
744
  const pendingSpacer = new Spacer(1);
757
745
  this.#multiFileBoxes.push(pendingSpacer);
758
746
  this.addChild(pendingSpacer);
759
- const pendingBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
747
+ const pendingBox = new Box(0, 0);
760
748
  const pendingText = renderStatusLine(
761
749
  {
762
750
  icon: "pending",
@@ -772,9 +760,8 @@ export class ToolExecutionComponent extends Container {
772
760
  } else {
773
761
  // Single-file or no result: standard rendering
774
762
  // Inline renderers skip background styling
775
- this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
763
+ this.#contentBox.setBgFn(undefined);
776
764
  this.#contentBox.clear();
777
- let contentBoxHasFramedBlock = false;
778
765
 
779
766
  const renderContext = this.#buildRenderContext();
780
767
  this.#renderState.renderContext = renderContext;
@@ -784,14 +771,11 @@ export class ToolExecutionComponent extends Container {
784
771
  // Render call component
785
772
  try {
786
773
  const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
787
- if (callComponent) {
788
- contentBoxHasFramedBlock =
789
- addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
790
- }
774
+ if (callComponent) this.#contentBox.addChild(callComponent);
791
775
  } catch (err) {
792
776
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
793
777
  // Fall back to default on error
794
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
778
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
795
779
  }
796
780
  }
797
781
 
@@ -808,24 +792,20 @@ export class ToolExecutionComponent extends Container {
808
792
  theme,
809
793
  this.#getCallArgsForRender(),
810
794
  );
811
- if (resultComponent) {
812
- contentBoxHasFramedBlock =
813
- addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
814
- }
795
+ if (resultComponent) this.#contentBox.addChild(resultComponent);
815
796
  } catch (err) {
816
797
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
817
798
  // Fall back to showing raw output on error
818
799
  const output = this.#getTextOutput();
819
800
  if (output) {
820
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
801
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
821
802
  }
822
803
  }
823
804
  }
824
- setBoxPaddingForFramedBlock(this.#contentBox, contentBoxHasFramedBlock);
825
805
  }
826
806
  } else {
827
807
  // Other built-in tools: use Text directly with caching
828
- this.#contentText.setCustomBgFn(bgFn);
808
+ this.#contentText.setCustomBgFn(stateBgFn);
829
809
  this.#contentText.setText(this.#formatToolExecution());
830
810
  }
831
811