@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,200 @@
1
+ /**
2
+ * Detect cache-mutating `gh` subcommands inside a bash invocation and drop
3
+ * the matching `github-cache` rows so a subsequent `issue://<n>` or
4
+ * `pr://<n>` read sees the post-mutation state instead of the stale
5
+ * pre-mutation snapshot.
6
+ *
7
+ * Triggered before the bash command runs: on success the cache is now
8
+ * empty and the next read fetches fresh; on failure the worst case is one
9
+ * extra `gh` round-trip on the following read. That cost is bounded and
10
+ * eliminates the much-worse "issue shows OPEN for up to softTtlSec after
11
+ * `gh issue close`" failure mode reported by users.
12
+ *
13
+ * Detector scope: ops that change visible issue/PR state — `close`,
14
+ * `reopen`, `merge`, `delete`, `ready`, `lock`, `unlock`, `pin`, `unpin`,
15
+ * `transfer`, plus the comment/review/edit ops that change the rendered
16
+ * body. We deliberately over-invalidate (e.g. all matching rows for the
17
+ * number, all auth_keys) because the upside of staleness elimination
18
+ * dwarfs the cost of one cache miss.
19
+ */
20
+ import { invalidateAllForNumber } from "./github-cache";
21
+
22
+ const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i;
23
+ const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/i;
24
+
25
+ /** Subcommands that mutate the rendered issue/PR view in any meaningful way. */
26
+ const MUTATING_ISSUE_SUBCMDS: Record<string, true> = {
27
+ close: true,
28
+ reopen: true,
29
+ delete: true,
30
+ edit: true,
31
+ comment: true,
32
+ lock: true,
33
+ unlock: true,
34
+ pin: true,
35
+ unpin: true,
36
+ transfer: true,
37
+ develop: true,
38
+ };
39
+
40
+ const MUTATING_PR_SUBCMDS: Record<string, true> = {
41
+ close: true,
42
+ reopen: true,
43
+ merge: true,
44
+ ready: true,
45
+ edit: true,
46
+ comment: true,
47
+ review: true,
48
+ lock: true,
49
+ unlock: true,
50
+ };
51
+ /**
52
+ * Walk a single shell command's token stream looking for a top-level
53
+ * `gh (issue|pr) <subcmd> <id-or-url>` invocation and return the
54
+ * invalidation key when one is found. Returns `null` for non-matching
55
+ * commands so the caller can iterate cheaply.
56
+ */
57
+ function detectGhMutation(tokens: readonly string[]): { number: number; repo?: string } | null {
58
+ const ghIdx = tokens.indexOf("gh");
59
+ if (ghIdx === -1) return null;
60
+ const subject = tokens[ghIdx + 1];
61
+ if (subject !== "issue" && subject !== "pr") return null;
62
+ const subcmd = tokens[ghIdx + 2];
63
+ if (!subcmd) return null;
64
+ const expected = subject === "issue" ? MUTATING_ISSUE_SUBCMDS : MUTATING_PR_SUBCMDS;
65
+ if (!expected[subcmd]) return null;
66
+
67
+ let repo: string | undefined;
68
+ // First pass: scan for --repo so it wins regardless of position relative
69
+ // to the issue/PR identifier (gh accepts the flag both before and after
70
+ // the positional argument).
71
+ for (let i = ghIdx + 3; i < tokens.length; i++) {
72
+ const token = tokens[i];
73
+ if (token === "-R" || token === "--repo") {
74
+ const next = tokens[i + 1];
75
+ if (next) repo = next;
76
+ i++;
77
+ continue;
78
+ }
79
+ if (token.startsWith("--repo=")) {
80
+ repo = token.slice("--repo=".length);
81
+ }
82
+ }
83
+ for (let i = ghIdx + 3; i < tokens.length; i++) {
84
+ const token = tokens[i];
85
+ if (token === "-R" || token === "--repo") {
86
+ i++;
87
+ continue;
88
+ }
89
+ if (token.startsWith("-")) continue;
90
+ const direct = /^\d+$/.test(token) ? Number(token) : undefined;
91
+ if (direct !== undefined && Number.isSafeInteger(direct) && direct > 0) {
92
+ return repo !== undefined ? { number: direct, repo } : { number: direct };
93
+ }
94
+ const urlMatch = (subject === "pr" ? PR_URL_PATTERN : ISSUE_URL_PATTERN).exec(token);
95
+ if (urlMatch) {
96
+ const num = Number(urlMatch[2]);
97
+ if (Number.isSafeInteger(num) && num > 0) {
98
+ // URL carries its own repo and wins over a stray --repo flag.
99
+ return { number: num, repo: urlMatch[1] };
100
+ }
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * Conservative tokenizer that splits a bash command into individual word
108
+ * tokens. Handles single/double-quoted strings, backslash escapes, and
109
+ * standard operators (`;`, `&&`, `||`, `|`, `&`, newlines) as token
110
+ * boundaries that emit a sentinel `";"` so the caller treats the segments
111
+ * as independent command sequences. We do not attempt full POSIX shell
112
+ * parsing — heredocs, command substitution, and arithmetic expansion are
113
+ * out of scope; the detector simply falls through when it cannot find a
114
+ * clean `gh issue|pr <subcmd>` triple.
115
+ */
116
+ function tokenize(command: string): string[][] {
117
+ const segments: string[][] = [];
118
+ let current: string[] = [];
119
+ let buffer = "";
120
+ let inSingle = false;
121
+ let inDouble = false;
122
+ const pushBuffer = () => {
123
+ if (buffer.length > 0) {
124
+ current.push(buffer);
125
+ buffer = "";
126
+ }
127
+ };
128
+ const pushSegment = () => {
129
+ pushBuffer();
130
+ if (current.length > 0) segments.push(current);
131
+ current = [];
132
+ };
133
+ for (let i = 0; i < command.length; i++) {
134
+ const ch = command[i];
135
+ if (inSingle) {
136
+ if (ch === "'") {
137
+ inSingle = false;
138
+ continue;
139
+ }
140
+ buffer += ch;
141
+ continue;
142
+ }
143
+ if (inDouble) {
144
+ if (ch === "\\" && i + 1 < command.length) {
145
+ const next = command[i + 1];
146
+ if (next === '"' || next === "\\" || next === "$" || next === "`") {
147
+ buffer += next;
148
+ i++;
149
+ continue;
150
+ }
151
+ }
152
+ if (ch === '"') {
153
+ inDouble = false;
154
+ continue;
155
+ }
156
+ buffer += ch;
157
+ continue;
158
+ }
159
+ if (ch === "'") {
160
+ inSingle = true;
161
+ continue;
162
+ }
163
+ if (ch === '"') {
164
+ inDouble = true;
165
+ continue;
166
+ }
167
+ if (ch === "\\" && i + 1 < command.length) {
168
+ buffer += command[i + 1];
169
+ i++;
170
+ continue;
171
+ }
172
+ if (ch === " " || ch === "\t") {
173
+ pushBuffer();
174
+ continue;
175
+ }
176
+ if (ch === "\n" || ch === ";" || ch === "&" || ch === "|" || ch === "(" || ch === ")") {
177
+ pushSegment();
178
+ // `&&`, `||` already collapsed by the segment break above.
179
+ continue;
180
+ }
181
+ buffer += ch;
182
+ }
183
+ pushSegment();
184
+ return segments;
185
+ }
186
+
187
+ /**
188
+ * Drop `github-cache` rows for any `gh issue|pr <mutating-subcmd>` call
189
+ * embedded in `command`. Safe to invoke unconditionally; no-op when the
190
+ * command does not touch GitHub state.
191
+ */
192
+ export function invalidateGithubCacheForBashCommand(command: string): void {
193
+ if (!command?.includes("gh")) return;
194
+ const segments = tokenize(command);
195
+ for (const segment of segments) {
196
+ const hit = detectGhMutation(segment);
197
+ if (!hit) continue;
198
+ invalidateAllForNumber(hit.number, hit.repo);
199
+ }
200
+ }
@@ -1,7 +1,7 @@
1
- import { type Component, padding, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import { type Component, padding, Text, visibleWidth } from "@oh-my-pi/pi-tui";
2
2
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
3
3
  import type { Theme, ThemeColor } from "../modes/theme/theme";
4
- import { renderStatusLine } from "../tui";
4
+ import { framedBlock, renderStatusLine } from "../tui";
5
5
  import type {
6
6
  GhRunWatchFailedLogDetails,
7
7
  GhRunWatchJobDetails,
@@ -239,7 +239,7 @@ function renderFailedLogs(
239
239
  return [];
240
240
  }
241
241
 
242
- const lines = ["", theme.fg("error", "failed logs")];
242
+ const lines: string[] = [];
243
243
  for (const entry of failedLogs) {
244
244
  const context = entry.workflowName ? `${entry.workflowName} #${entry.runId}` : `run #${entry.runId}`;
245
245
  lines.push(
@@ -268,36 +268,45 @@ function renderFailedLogs(
268
268
  return lines;
269
269
  }
270
270
 
271
- function buildWatchLines(
271
+ function buildWatchSections(
272
272
  watch: GhRunWatchViewDetails,
273
273
  theme: Theme,
274
274
  options: RenderResultOptions,
275
275
  width: number,
276
- ): string[] {
277
- const lines = [theme.fg("muted", getWatchHeader(watch))];
276
+ ): Array<{ label?: string; lines: string[] }> {
277
+ const main: string[] = [];
278
278
 
279
279
  if (watch.note) {
280
- lines.push(theme.fg("dim", replaceTabs(watch.note)));
280
+ main.push(theme.fg("dim", replaceTabs(watch.note)));
281
281
  }
282
282
 
283
283
  if (watch.mode === "run" && watch.run) {
284
- lines.push(...renderRunBlock(watch.run, width, theme));
284
+ main.push(...renderRunBlock(watch.run, width, theme));
285
285
  } else if (watch.mode === "commit") {
286
286
  const runs = watch.runs ?? [];
287
287
  if (runs.length === 0) {
288
- lines.push(theme.fg("dim", "waiting for workflow runs..."));
288
+ main.push(theme.fg("dim", "waiting for workflow runs..."));
289
289
  } else {
290
290
  runs.forEach((run, index) => {
291
291
  if (index > 0) {
292
- lines.push("");
292
+ main.push("");
293
293
  }
294
- lines.push(...renderRunBlock(run, width, theme));
294
+ main.push(...renderRunBlock(run, width, theme));
295
295
  });
296
296
  }
297
297
  }
298
298
 
299
- lines.push(...renderFailedLogs(watch.failedLogs ?? [], width, theme, options.expanded));
300
- return lines;
299
+ const sections: Array<{ label?: string; lines: string[] }> = [];
300
+ if (main.length > 0) {
301
+ sections.push({ lines: main });
302
+ }
303
+
304
+ const failed = renderFailedLogs(watch.failedLogs ?? [], width, theme, options.expanded);
305
+ if (failed.length > 0) {
306
+ sections.push({ label: "failed logs", lines: failed });
307
+ }
308
+
309
+ return sections;
301
310
  }
302
311
 
303
312
  function extractText(content: Array<{ type: string; text?: string }>): string {
@@ -335,29 +344,44 @@ function renderFallbackComponent(
335
344
  }
336
345
 
337
346
  const allLines = replaceTabs(text).split("\n");
347
+ while (allLines.length > 0 && allLines[0].trim() === "") allLines.shift();
348
+ while (allLines.length > 0 && allLines[allLines.length - 1].trim() === "") allLines.pop();
349
+
350
+ // Trivial one-line *success* result: a clean status line beats an almost-empty box.
351
+ // Errors always frame so the message reads as a structured block, never a raw red wrap.
352
+ if (allLines.length <= 1 && !isError) {
353
+ const body = allLines[0];
354
+ if (!body) return new Text(header, 0, 0);
355
+ const colored = isError ? theme.fg("error", body) : theme.fg("toolOutput", body);
356
+ return new Text(`${header}\n${colored}`, 0, 0);
357
+ }
338
358
 
339
- return {
340
- render(width: number): string[] {
341
- const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
342
- const expanded = options.expanded;
343
- const limit = expanded ? allLines.length : Math.min(allLines.length, PREVIEW_LIMITS.OUTPUT_EXPANDED);
344
- const visible = allLines.slice(0, limit);
345
- const remaining = allLines.length - visible.length;
346
-
347
- const out: string[] = [header];
348
- for (const line of visible) {
349
- const colored = isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
350
- out.push(truncateVisualWidth(colored, lineWidth));
351
- }
352
- if (!expanded && remaining > 0) {
353
- const hint = formatExpandHint(theme, expanded, true);
354
- const more = `${formatMoreItems(remaining, "line")}${hint ? ` ${hint}` : ""}`;
355
- out.push(theme.fg("dim", more));
356
- }
357
- return out.map(line => truncateToWidth(line, lineWidth));
358
- },
359
- invalidate() {},
360
- };
359
+ return framedBlock(theme, width => {
360
+ const lineWidth = Math.max(1, (width || FALLBACK_WIDTH) - 3);
361
+ const expanded = options.expanded;
362
+ const limit = expanded ? allLines.length : Math.min(allLines.length, PREVIEW_LIMITS.OUTPUT_EXPANDED);
363
+ const visible = allLines.slice(0, limit);
364
+ const remaining = allLines.length - visible.length;
365
+
366
+ const out: string[] = [];
367
+ for (const line of visible) {
368
+ const colored = isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
369
+ out.push(truncateVisualWidth(colored, lineWidth));
370
+ }
371
+ if (!expanded && remaining > 0) {
372
+ const hint = formatExpandHint(theme, expanded, true);
373
+ const more = `${formatMoreItems(remaining, "line")}${hint ? ` ${hint}` : ""}`;
374
+ out.push(theme.fg("dim", more));
375
+ }
376
+ return {
377
+ header,
378
+ sections: out.length > 0 ? [{ lines: out }] : [],
379
+ state: isError ? "error" : "success",
380
+ borderColor: isError ? "error" : "borderMuted",
381
+ applyBg: false,
382
+ width,
383
+ };
384
+ });
361
385
  }
362
386
 
363
387
  function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOptions, theme: Theme): Component {
@@ -380,7 +404,7 @@ function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOption
380
404
  }
381
405
 
382
406
  const header = `${icon} ${titleText} ${metaText}`;
383
- const wait = theme.fg("dim", " waiting for workflow data...");
407
+ const wait = theme.fg("dim", "waiting for workflow data...");
384
408
  return new Text(`${header}\n${wait}`, 0, 0);
385
409
  }
386
410
 
@@ -412,13 +436,28 @@ export const githubToolRenderer = {
412
436
  ): Component {
413
437
  const watch = result.details?.watch;
414
438
  if (watch) {
415
- return {
416
- render(width: number): string[] {
417
- const lineWidth = Math.max(24, width || FALLBACK_WIDTH);
418
- return buildWatchLines(watch, uiTheme, options, lineWidth).map(line => truncateToWidth(line, lineWidth));
439
+ const isError = result.isError === true;
440
+ const header = renderStatusLine(
441
+ {
442
+ icon: isError ? "error" : "success",
443
+ title: "GitHub Run Watch",
444
+ titleColor: isError ? "error" : "accent",
445
+ meta: [getWatchHeader(watch)],
419
446
  },
420
- invalidate() {},
421
- };
447
+ uiTheme,
448
+ );
449
+ return framedBlock(uiTheme, width => {
450
+ const innerWidth = Math.max(1, (width || FALLBACK_WIDTH) - 3);
451
+ const sections = buildWatchSections(watch, uiTheme, options, innerWidth);
452
+ return {
453
+ header,
454
+ sections,
455
+ state: isError ? "error" : "success",
456
+ borderColor: isError ? "error" : "borderMuted",
457
+ applyBg: false,
458
+ width,
459
+ };
460
+ });
422
461
  }
423
462
 
424
463
  return renderFallbackComponent(result, options, uiTheme, args ?? {});
@@ -316,6 +316,31 @@ export function invalidate(
316
316
  }
317
317
  }
318
318
 
319
+ /**
320
+ * Drop every cached row for a given issue/PR number, regardless of repo,
321
+ * auth key, include_comments flag, or row kind ({@link CacheKind}). Best-effort:
322
+ * swallows DB failures the same way {@link invalidate} does.
323
+ *
324
+ * Used by the bash-side detector that reacts to `gh issue close` / `gh pr merge`
325
+ * style mutations. Repo + auth-key narrowing is intentionally skipped because
326
+ * the bash command often does not name the repo (defaults to cwd's `gh`
327
+ * config) and resolving the *current* repo from `cwd` for every bash call would
328
+ * be far more expensive than a write-amplified DELETE.
329
+ */
330
+ export function invalidateAllForNumber(number: number, repo?: string): void {
331
+ const db = openDb();
332
+ if (!db) return;
333
+ try {
334
+ if (repo === undefined) {
335
+ db.prepare("DELETE FROM github_view_cache WHERE number = ?").run(number);
336
+ } else {
337
+ db.prepare("DELETE FROM github_view_cache WHERE number = ? AND repo = ?").run(number, normalizeRepo(repo));
338
+ }
339
+ } catch (err) {
340
+ logger.debug("github cache: invalidateAllForNumber failed", { err: String(err) });
341
+ }
342
+ }
343
+
319
344
  /** Drop every cached row. Test helper. */
320
345
  export function clearAll(): void {
321
346
  const db = openDb();