@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.1

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 (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -14,7 +14,7 @@ import type {
14
14
  import type { CompactOptions } from "../extensibility/extensions/types";
15
15
  import type { MCPManager } from "../mcp";
16
16
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
17
- import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
17
+ import type { AgentSession } from "../session/agent-session";
18
18
  import type { HistoryStorage } from "../session/history-storage";
19
19
  import type { SessionContext, SessionManager } from "../session/session-manager";
20
20
  import type { ShakeMode } from "../session/shake-types";
@@ -63,6 +63,7 @@ export type TodoPhase = {
63
63
 
64
64
  export interface InteractiveModeInitOptions {
65
65
  suppressWelcomeIntro?: boolean;
66
+ clearInitialTerminalHistory?: boolean;
66
67
  }
67
68
 
68
69
  export type InteractiveSelectorDialogOptions = ExtensionUIDialogOptions & Pick<HookSelectorOptions, "disabledIndices">;
@@ -95,7 +96,6 @@ export interface InteractiveModeContext {
95
96
 
96
97
  // State
97
98
  isInitialized: boolean;
98
- isBackgrounded: boolean;
99
99
  isBashMode: boolean;
100
100
  toolOutputExpanded: boolean;
101
101
  todoExpanded: boolean;
@@ -149,15 +149,25 @@ export interface InteractiveModeContext {
149
149
  // Extension UI integration
150
150
  setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
151
151
  initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
152
- createBackgroundUiContext(): ExtensionUIContext;
153
152
  setEditorComponent(
154
153
  factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
155
154
  ): void;
156
155
 
157
- // Event handling
158
- handleBackgroundEvent(event: AgentSessionEvent): Promise<void>;
159
-
160
156
  // UI helpers
157
+ /**
158
+ * Mount transcript content and repaint once. The single sink for "show this in
159
+ * chat": producers build and return a `Component` (or a `ChatBlock` carrying
160
+ * its own lifecycle) and hand it here instead of touching `chatContainer` /
161
+ * `ui.requestRender()` directly. `ChatBlock`s are mounted (their `onMount`
162
+ * runs) so their timers/subscriptions start.
163
+ */
164
+ present(content: Component | readonly Component[]): void;
165
+ /**
166
+ * Dispose every live block in the transcript (stopping timers/subscriptions)
167
+ * and clear it. Used before a full rebuild so animated/streaming blocks do not
168
+ * leak.
169
+ */
170
+ resetTranscript(): void;
161
171
  showStatus(message: string, options?: { dim?: boolean }): void;
162
172
  showError(message: string): void;
163
173
  showPinnedError(message: string): void;
@@ -233,6 +243,7 @@ export interface InteractiveModeContext {
233
243
  handleDumpCommand(): void;
234
244
  handleDebugTranscriptCommand(): Promise<void>;
235
245
  handleClearCommand(): Promise<void>;
246
+ handleFreshCommand(): Promise<void>;
236
247
  handleDropCommand(): Promise<void>;
237
248
  handleForkCommand(): Promise<void>;
238
249
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
@@ -269,7 +280,7 @@ export interface InteractiveModeContext {
269
280
  handleSessionDeleteCommand(): Promise<void>;
270
281
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
271
282
  showHookConfirm(title: string, message: string): Promise<boolean>;
272
- showDebugSelector(): void;
283
+ showDebugSelector(): Promise<void>;
273
284
  showSessionObserver(): void;
274
285
  resetObserverRegistry(): void;
275
286
 
@@ -278,9 +289,9 @@ export interface InteractiveModeContext {
278
289
  handleCtrlD(): void;
279
290
  handleCtrlZ(): void;
280
291
  handleDequeue(): void;
281
- handleBackgroundCommand(): void;
282
292
  handleImagePaste(): Promise<boolean>;
283
293
  handleBtwCommand(question: string): Promise<void>;
294
+ handleTanCommand(work: string): Promise<void>;
284
295
  hasActiveBtw(): boolean;
285
296
  handleBtwEscape(): boolean;
286
297
  handleOmfgCommand(complaint: string): Promise<void>;
@@ -37,6 +37,9 @@ export interface ContextBreakdown {
37
37
  freeTokens: number;
38
38
  }
39
39
 
40
+ const EMPTY_STRING_PARTS: readonly string[] = [];
41
+ const EMPTY_TOOLS: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">> = [];
42
+
40
43
  export function estimateSkillsTokens(skills: readonly Skill[]): number {
41
44
  const fragments: string[] = [];
42
45
  for (const skill of skills) {
@@ -75,15 +78,16 @@ export function estimateToolSchemaTokens(
75
78
  * messages walked incrementally as new entries append.
76
79
  */
77
80
  export function computeNonMessageTokens(session: AgentSession): number {
78
- const parts = computeNonMessageBreakdown(session);
79
- return parts.systemPromptTokens + parts.systemContextTokens + parts.toolsTokens + parts.skillsTokens;
81
+ const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
82
+ const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
83
+ return countTokens(systemPromptParts) + estimateToolSchemaTokens(tools);
80
84
  }
81
85
 
82
86
  /**
83
- * Shared helper for the four non-message token totals. Single source of truth
84
- * for both `computeNonMessageTokens` (status-line incremental cache) and
85
- * `computeContextBreakdown` (/context panel). The split avoids drift between
86
- * the two surfaces — they MUST report the same numbers.
87
+ * Shared helper for the four non-message token totals used by
88
+ * `computeContextBreakdown` (/context panel). Keep this category split stable:
89
+ * the status-line fast path intentionally uses the equivalent collapsed total
90
+ * in `computeNonMessageTokens`.
87
91
  */
88
92
  function computeNonMessageBreakdown(session: AgentSession): {
89
93
  skillsTokens: number;
@@ -9,6 +9,15 @@ export interface CodeBlock {
9
9
  code: string;
10
10
  }
11
11
 
12
+ /** A blockquote block: a maximal run of `>`-prefixed lines from markdown. */
13
+ export interface QuoteBlock {
14
+ /** Block body with each line's `>` marker (and one optional space) removed. */
15
+ text: string;
16
+ }
17
+
18
+ /** A drillable block within an assistant message, in document order. */
19
+ export type MessageBlock = ({ kind: "code" } & CodeBlock) | ({ kind: "quote" } & QuoteBlock);
20
+
12
21
  /** A runnable command found in the transcript. */
13
22
  export interface LastCommand {
14
23
  kind: "bash" | "eval";
@@ -23,7 +32,7 @@ export interface LastCommand {
23
32
  * `children` to drill into.
24
33
  */
25
34
  export interface CopyTarget {
26
- /** Stable identifier (e.g. "msg:1", "msg:1:code:0", "msg:1:all", "cmd:1"). */
35
+ /** Stable id (e.g. "msg:1", "msg:1:code:0", "msg:1:quote:0", "msg:1:all", "cmd:1"). */
27
36
  id: string;
28
37
  label: string;
29
38
  /** Dim annotation: line/block counts, language, or tool name. */
@@ -49,17 +58,73 @@ export interface CopySource {
49
58
  /** Cap on how many recent assistant messages the picker lists. */
50
59
  const MAX_MESSAGES = 50;
51
60
 
52
- const CODE_BLOCK_RE = /^```([^\n]*)\n([\s\S]*?)^```/gm;
61
+ const OPEN_FENCE_RE = /^```([^\n]*)$/;
62
+ const CLOSE_FENCE_RE = /^```/;
63
+ const QUOTE_LINE_RE = /^>(.*)$/;
53
64
 
54
- /** Extract fenced code blocks from assistant markdown, in document order. */
55
- export function extractCodeBlocks(text: string): CodeBlock[] {
56
- const blocks: CodeBlock[] = [];
57
- for (const match of text.matchAll(CODE_BLOCK_RE)) {
58
- blocks.push({ lang: match[1].trim(), code: match[2].replace(/\n$/, "") });
65
+ /**
66
+ * Split assistant markdown into drillable blocks — fenced code and `>`-quoted
67
+ * runs in document order. Fences mask their bodies, so a `>` line inside a
68
+ * code block is never mistaken for a quote. An unclosed fence is treated as
69
+ * ordinary text, matching the fenced-block grammar.
70
+ */
71
+ export function extractBlocks(text: string): MessageBlock[] {
72
+ const blocks: MessageBlock[] = [];
73
+ const lines = text.split("\n");
74
+ let quote: string[] | undefined;
75
+ const flushQuote = () => {
76
+ if (quote) {
77
+ blocks.push({ kind: "quote", text: quote.join("\n") });
78
+ quote = undefined;
79
+ }
80
+ };
81
+
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i]!;
84
+ const open = OPEN_FENCE_RE.exec(line);
85
+ if (open) {
86
+ let close = -1;
87
+ for (let k = i + 1; k < lines.length; k++) {
88
+ if (CLOSE_FENCE_RE.test(lines[k]!)) {
89
+ close = k;
90
+ break;
91
+ }
92
+ }
93
+ if (close !== -1) {
94
+ flushQuote();
95
+ blocks.push({ kind: "code", lang: open[1].trim(), code: lines.slice(i + 1, close).join("\n") });
96
+ i = close;
97
+ continue;
98
+ }
99
+ }
100
+
101
+ const quoted = QUOTE_LINE_RE.exec(line);
102
+ if (quoted) {
103
+ // Strip the `>` marker plus one optional following space.
104
+ quote ??= [];
105
+ quote.push(quoted[1].startsWith(" ") ? quoted[1].slice(1) : quoted[1]);
106
+ } else {
107
+ flushQuote();
108
+ }
59
109
  }
110
+ flushQuote();
60
111
  return blocks;
61
112
  }
62
113
 
114
+ /** Extract fenced code blocks from assistant markdown, in document order. */
115
+ export function extractCodeBlocks(text: string): CodeBlock[] {
116
+ return extractBlocks(text)
117
+ .filter((b): b is { kind: "code" } & CodeBlock => b.kind === "code")
118
+ .map(b => ({ lang: b.lang, code: b.code }));
119
+ }
120
+
121
+ /** Extract `>`-quoted blocks from assistant markdown, in document order. */
122
+ export function extractQuoteBlocks(text: string): QuoteBlock[] {
123
+ return extractBlocks(text)
124
+ .filter((b): b is { kind: "quote" } & QuoteBlock => b.kind === "quote")
125
+ .map(b => ({ text: b.text }));
126
+ }
127
+
63
128
  function extractEvalCode(args: unknown): { code: string; language: string } | undefined {
64
129
  if (!args || typeof args !== "object") return undefined;
65
130
  const cells = (args as { cells?: unknown }).cells;
@@ -136,42 +201,83 @@ function firstLine(text: string): string {
136
201
  return text.trim().replace(/\s+/g, " ");
137
202
  }
138
203
 
139
- /** Build the target node for one assistant message: a leaf when it has no code
140
- * blocks, otherwise a group exposing the full message, each block, and "all". */
204
+ /** "<n> lines · <c> code · <q> quote" omitting block kinds that are absent. */
205
+ function blockSummaryHint(text: string, codeCount: number, quoteCount: number): string {
206
+ const parts = [pluralLines(text)];
207
+ if (codeCount > 0) parts.push(`${codeCount} code`);
208
+ if (quoteCount > 0) parts.push(`${quoteCount} quote`);
209
+ return parts.join(" · ");
210
+ }
211
+
212
+ /** Build the target node for one assistant message: a leaf when it has no
213
+ * drillable blocks, otherwise a group exposing the full message plus each
214
+ * fenced code block and `>`-quoted block (de-prefixed) as a child target. */
141
215
  function messageTarget(text: string, rank: number): CopyTarget {
142
216
  const id = `msg:${rank}`;
143
217
  const label = firstLine(text);
144
- const blocks = extractCodeBlocks(text);
145
- const hint = blocks.length > 0 ? `${pluralLines(text)} · ${blocks.length} code` : pluralLines(text);
218
+ const blocks = extractBlocks(text);
146
219
  const messageCopy = rank === 1 ? "Copied last message to clipboard" : "Copied message to clipboard";
147
220
 
148
221
  if (blocks.length === 0) {
149
- return { id, label, hint, preview: text, content: text, copyMessage: messageCopy };
222
+ return { id, label, hint: pluralLines(text), preview: text, content: text, copyMessage: messageCopy };
223
+ }
224
+
225
+ // The message node itself copies the full message; each block is a child
226
+ // copy target you can drill into, kept in document order.
227
+ const children: CopyTarget[] = [];
228
+ const codeBlocks: CodeBlock[] = [];
229
+ const quoteBlocks: QuoteBlock[] = [];
230
+ for (const block of blocks) {
231
+ if (block.kind === "code") {
232
+ const j = codeBlocks.length;
233
+ codeBlocks.push(block);
234
+ children.push({
235
+ id: `${id}:code:${j}`,
236
+ label: `Block ${j + 1}`,
237
+ hint: blockHint(block),
238
+ preview: block.code,
239
+ language: block.lang || undefined,
240
+ content: block.code,
241
+ copyMessage: `Copied code block ${j + 1} to clipboard`,
242
+ });
243
+ } else {
244
+ const j = quoteBlocks.length;
245
+ quoteBlocks.push(block);
246
+ children.push({
247
+ id: `${id}:quote:${j}`,
248
+ label: `Quote ${j + 1}`,
249
+ hint: pluralLines(block.text),
250
+ preview: block.text,
251
+ content: block.text,
252
+ copyMessage: `Copied quote block ${j + 1} to clipboard`,
253
+ });
254
+ }
150
255
  }
151
256
 
152
- // The message node itself copies the full message; its code blocks are
153
- // child copy targets you can expand into.
154
- const children: CopyTarget[] = blocks.map((block, j) => ({
155
- id: `${id}:code:${j}`,
156
- label: `Block ${j + 1}`,
157
- hint: blockHint(block),
158
- preview: block.code,
159
- language: block.lang || undefined,
160
- content: block.code,
161
- copyMessage: `Copied code block ${j + 1} to clipboard`,
162
- }));
163
- if (blocks.length > 1) {
164
- const combined = blocks.map(b => b.code).join("\n\n");
257
+ if (codeBlocks.length > 1) {
258
+ const combined = codeBlocks.map(b => b.code).join("\n\n");
165
259
  children.push({
166
260
  id: `${id}:all`,
167
- label: `All ${blocks.length} blocks`,
261
+ label: `All ${codeBlocks.length} blocks`,
262
+ hint: pluralLines(combined),
263
+ preview: combined,
264
+ content: combined,
265
+ copyMessage: `Copied ${codeBlocks.length} code blocks to clipboard`,
266
+ });
267
+ }
268
+ if (quoteBlocks.length > 1) {
269
+ const combined = quoteBlocks.map(b => b.text).join("\n\n");
270
+ children.push({
271
+ id: `${id}:all-quotes`,
272
+ label: `All ${quoteBlocks.length} quotes`,
168
273
  hint: pluralLines(combined),
169
274
  preview: combined,
170
275
  content: combined,
171
- copyMessage: `Copied ${blocks.length} code blocks to clipboard`,
276
+ copyMessage: `Copied ${quoteBlocks.length} quote blocks to clipboard`,
172
277
  });
173
278
  }
174
279
 
280
+ const hint = blockSummaryHint(text, codeBlocks.length, quoteBlocks.length);
175
281
  return { id, label, hint, preview: text, content: text, copyMessage: messageCopy, children };
176
282
  }
177
283
 
@@ -37,6 +37,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
37
37
  `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
38
38
  `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
39
39
  `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
40
+ `| \`${appKey(bindings, "app.display.reset")}\` | Reset terminal display |`,
40
41
  `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
41
42
  `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle role models (slow/default/smol) |`,
42
43
  `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle role models (backward) |`,
@@ -17,6 +17,7 @@ import {
17
17
  } from "../../modes/components/read-tool-group";
18
18
  import { SkillMessageComponent } from "../../modes/components/skill-message";
19
19
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
20
+ import { TranscriptBlock } from "../../modes/components/transcript-container";
20
21
  import { UserMessageComponent } from "../../modes/components/user-message";
21
22
  import { materializeImageReferenceLinksSync } from "../../modes/image-references";
22
23
  import { theme } from "../../modes/theme/theme";
@@ -24,6 +25,7 @@ import type { CompactionQueuedMessage, InteractiveModeContext } from "../../mode
24
25
  import {
25
26
  type CustomMessage,
26
27
  isSilentAbort,
28
+ resolveAbortLabel,
27
29
  SKILL_PROMPT_MESSAGE_TYPE,
28
30
  type SkillPromptDetails,
29
31
  } from "../../session/messages";
@@ -73,9 +75,6 @@ export class UiHelpers {
73
75
  * we update the previous status line instead of appending new ones to avoid log spam.
74
76
  */
75
77
  showStatus(message: string, options?: { dim?: boolean }): void {
76
- if (this.ctx.isBackgrounded) {
77
- return;
78
- }
79
78
  const children = this.ctx.chatContainer.children;
80
79
  const last = children.length > 0 ? children[children.length - 1] : undefined;
81
80
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
@@ -90,11 +89,9 @@ export class UiHelpers {
90
89
 
91
90
  const spacer = new Spacer(1);
92
91
  const text = new Text(rendered, 1, 0);
93
- this.ctx.chatContainer.addChild(spacer);
94
- this.ctx.chatContainer.addChild(text);
92
+ this.ctx.present([spacer, text]);
95
93
  this.ctx.lastStatusSpacer = spacer;
96
94
  this.ctx.lastStatusText = text;
97
- this.ctx.ui.requestRender();
98
95
  }
99
96
 
100
97
  addMessageToChat(
@@ -153,6 +150,7 @@ export class UiHelpers {
153
150
  durationMs: details?.durationMs,
154
151
  },
155
152
  ];
153
+ const block = new TranscriptBlock();
156
154
  for (const job of jobs) {
157
155
  const jobId = job.jobId ?? "unknown";
158
156
  const typeLabel = job.type ? `[${job.type}]` : "[job]";
@@ -165,8 +163,9 @@ export class UiHelpers {
165
163
  ]
166
164
  .filter(Boolean)
167
165
  .join(" ");
168
- this.ctx.chatContainer.addChild(new Text(line, 1, 0));
166
+ block.addChild(new Text(line, 1, 0));
169
167
  }
168
+ this.ctx.chatContainer.addChild(block);
170
169
  break;
171
170
  }
172
171
  if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
@@ -206,19 +205,18 @@ export class UiHelpers {
206
205
  body = details?.body ?? "";
207
206
  arrow = `${from} ⇨ ${to}`;
208
207
  }
209
- const components: Component[] = [];
208
+ const block = new TranscriptBlock();
210
209
  const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
211
210
  const headerComponent = new Text(header, 1, 0);
212
- this.ctx.chatContainer.addChild(headerComponent);
213
- components.push(headerComponent);
211
+ block.addChild(headerComponent);
214
212
  if (body) {
215
213
  for (const line of body.split("\n")) {
216
214
  const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
217
- this.ctx.chatContainer.addChild(lineComponent);
218
- components.push(lineComponent);
215
+ block.addChild(lineComponent);
219
216
  }
220
217
  }
221
- return components;
218
+ this.ctx.chatContainer.addChild(block);
219
+ return [block];
222
220
  }
223
221
  const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
224
222
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
@@ -229,14 +227,12 @@ export class UiHelpers {
229
227
  break;
230
228
  }
231
229
  case "compactionSummary": {
232
- this.ctx.chatContainer.addChild(new Spacer(1));
233
230
  const component = new CompactionSummaryMessageComponent(message);
234
231
  component.setExpanded(this.ctx.toolOutputExpanded);
235
232
  this.ctx.chatContainer.addChild(component);
236
233
  break;
237
234
  }
238
235
  case "branchSummary": {
239
- this.ctx.chatContainer.addChild(new Spacer(1));
240
236
  const component = new BranchSummaryMessageComponent(message);
241
237
  component.setExpanded(this.ctx.toolOutputExpanded);
242
238
  this.ctx.chatContainer.addChild(component);
@@ -244,6 +240,7 @@ export class UiHelpers {
244
240
  }
245
241
  case "fileMention": {
246
242
  // Render compact file mention display
243
+ const block = new TranscriptBlock();
247
244
  for (const file of message.files) {
248
245
  let suffix: string;
249
246
  if (file.skippedReason === "tooLarge") {
@@ -260,8 +257,9 @@ export class UiHelpers {
260
257
  "accent",
261
258
  file.path,
262
259
  )} ${theme.fg("dim", suffix)}`;
263
- this.ctx.chatContainer.addChild(new Text(text, 0, 0));
260
+ block.addChild(new Text(text, 0, 0));
264
261
  }
262
+ if (block.children.length > 0) this.ctx.chatContainer.addChild(block);
265
263
  break;
266
264
  }
267
265
  case "user":
@@ -338,18 +336,21 @@ export class UiHelpers {
338
336
  if (assistantComponent) {
339
337
  assistantComponent.setUsageInfo(message.usage);
340
338
  }
341
- readGroup = null;
339
+ const hasVisibleAssistantContent = message.content.some(
340
+ content =>
341
+ (content.type === "text" && content.text.trim().length > 0) ||
342
+ (content.type === "thinking" && content.thinking.trim().length > 0),
343
+ );
344
+ if (hasVisibleAssistantContent) {
345
+ readGroup?.finalize();
346
+ readGroup = null;
347
+ }
342
348
  const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
343
349
  const hasErrorStop =
344
350
  !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
345
351
  const errorMessage = hasErrorStop
346
352
  ? message.stopReason === "aborted"
347
- ? (() => {
348
- const retryAttempt = this.ctx.session.retryAttempt;
349
- return retryAttempt > 0
350
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
351
- : "Operation aborted";
352
- })()
353
+ ? resolveAbortLabel(message.errorMessage, this.ctx.session.retryAttempt)
353
354
  : message.errorMessage || "Error"
354
355
  : null;
355
356
 
@@ -391,6 +392,7 @@ export class UiHelpers {
391
392
  continue;
392
393
  }
393
394
 
395
+ readGroup?.finalize();
394
396
  readGroup = null;
395
397
  const tool = this.ctx.session.getToolByName(content.name);
396
398
  const renderArgs =
@@ -478,6 +480,10 @@ export class UiHelpers {
478
480
  }
479
481
  }
480
482
 
483
+ // The trailing read run has no following break to close it; finalize so the
484
+ // rebuilt group commits to native scrollback like every other historical block.
485
+ readGroup?.finalize();
486
+
481
487
  // Render deferred messages (compaction summaries) at the bottom so they're visible
482
488
  for (const message of deferredMessages) {
483
489
  this.ctx.addMessageToChat(message, options);
@@ -490,8 +496,15 @@ export class UiHelpers {
490
496
  renderInitialMessages(prebuiltContext?: SessionContext, options: RenderInitialMessagesOptions = {}): void {
491
497
  // This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI).
492
498
  // Clear existing rendered chat first to avoid duplicating the full session in the container.
499
+ // On a non-preserving rebuild the existing blocks are discarded for good, so
500
+ // dispose them (stopping any live timers/subscriptions) before clearing. When
501
+ // preserving, the same instances are re-added below, so detach without dispose.
493
502
  const preservedChatChildren = options.preserveExistingChat ? this.ctx.chatContainer.children : undefined;
494
- this.ctx.chatContainer.clear();
503
+ if (preservedChatChildren) {
504
+ this.ctx.chatContainer.clear();
505
+ } else {
506
+ this.ctx.resetTranscript();
507
+ }
495
508
  this.ctx.pendingMessagesContainer.clear();
496
509
  this.ctx.pendingBashComponents = [];
497
510
  this.ctx.pendingPythonComponents = [];
@@ -527,9 +540,6 @@ export class UiHelpers {
527
540
  }
528
541
 
529
542
  clearEditor(): void {
530
- if (this.ctx.isBackgrounded) {
531
- return;
532
- }
533
543
  this.ctx.editor.setText("");
534
544
  this.ctx.pendingImages = [];
535
545
  this.ctx.pendingImageLinks = [];
@@ -538,29 +548,17 @@ export class UiHelpers {
538
548
  }
539
549
 
540
550
  showError(errorMessage: string): void {
541
- if (this.ctx.isBackgrounded) {
542
- process.stderr.write(`Error: ${errorMessage}\n`);
543
- return;
544
- }
545
- this.ctx.chatContainer.addChild(new Spacer(1));
546
- this.ctx.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
547
- this.ctx.ui.requestRender();
551
+ this.ctx.present([new Spacer(1), new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)]);
548
552
  }
549
553
 
550
554
  showWarning(warningMessage: string): void {
551
- if (this.ctx.isBackgrounded) {
552
- process.stderr.write(`Warning: ${warningMessage}\n`);
553
- return;
554
- }
555
- this.ctx.chatContainer.addChild(new Spacer(1));
556
- this.ctx.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
557
- this.ctx.ui.requestRender();
555
+ this.ctx.present([new Spacer(1), new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0)]);
558
556
  }
559
557
 
560
558
  showNewVersionNotification(newVersion: string): void {
561
- this.ctx.chatContainer.addChild(new Spacer(1));
562
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
563
- this.ctx.chatContainer.addChild(
559
+ const block = new TranscriptBlock();
560
+ block.addChild(new DynamicBorder(text => theme.fg("warning", text)));
561
+ block.addChild(
564
562
  new Text(
565
563
  theme.bold(theme.fg("warning", "Update Available")) +
566
564
  "\n" +
@@ -570,8 +568,8 @@ export class UiHelpers {
570
568
  0,
571
569
  ),
572
570
  );
573
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
574
- this.ctx.ui.requestRender();
571
+ block.addChild(new DynamicBorder(text => theme.fg("warning", text)));
572
+ this.ctx.present(block);
575
573
  }
576
574
 
577
575
  updatePendingMessagesDisplay(): void {