@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.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 (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
@@ -36,6 +36,16 @@ export class AssistantMessageComponent extends Container {
36
36
  * transcript keeps the error in history.
37
37
  */
38
38
  #errorPinned = false;
39
+ /**
40
+ * Monotonic content version reported to the transcript container via
41
+ * {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
42
+ * choke point every mutator funnels through, including the post-finalize
43
+ * ones: `setErrorPinned(false)` restoring the inline error at the next
44
+ * turn's `agent_start`, late tool-result images, async Kitty conversions,
45
+ * and `setUsageInfo`. Without it, the container's committed-scrollback
46
+ * bypass would replay this block's pre-mutation bytes forever.
47
+ */
48
+ #blockVersion = 0;
39
49
  /** Whether the last updateContent carried an in-flight streaming partial; such
40
50
  * renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
41
51
  #lastUpdateTransient = false;
@@ -86,6 +96,10 @@ export class AssistantMessageComponent extends Container {
86
96
  return this.#transcriptBlockFinalized;
87
97
  }
88
98
 
99
+ getTranscriptBlockVersion(): number {
100
+ return this.#blockVersion;
101
+ }
102
+
89
103
  markTranscriptBlockFinalized(): void {
90
104
  this.#transcriptBlockFinalized = true;
91
105
  }
@@ -215,6 +229,7 @@ export class AssistantMessageComponent extends Container {
215
229
  }
216
230
 
217
231
  updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
232
+ this.#blockVersion++;
218
233
  this.#lastMessage = message;
219
234
  this.#lastUpdateTransient = opts?.transient === true;
220
235
 
@@ -73,7 +73,11 @@ export class BtwPanelComponent extends Container {
73
73
  this.addChild(new Text(this.#footerLine(), 1, 0));
74
74
  this.addChild(new Spacer(1));
75
75
  this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
76
- this.#tui.requestRender();
76
+ // Component-scoped: a rebuild replaces only this panel's own children
77
+ // (streaming deltas arrive per token, and a full compose would re-walk
78
+ // the whole transcript each time). Before the panel is mounted the TUI
79
+ // cannot resolve it and falls back to a full compose on its own.
80
+ this.#tui.requestComponentRender(this);
77
81
  }
78
82
 
79
83
  #footerLine(): string {
@@ -1,51 +1,87 @@
1
- import { Box, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
1
+ import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
3
  import type { CompactionSummaryMessage } from "../../session/messages";
4
4
 
5
5
  /**
6
- * Component that renders a compaction message with collapsed/expanded state.
7
- * Uses same background color as hook messages for visual consistency.
6
+ * Compaction point in the transcript, rendered as a slim horizontal divider:
7
+ *
8
+ * ──────── 📷 compacted · ctrl+o ────────
9
+ *
10
+ * The conversation above the divider stays visible (display transcript keeps
11
+ * full history); only the LLM context was reset. Expanding (ctrl+o) reveals
12
+ * the compaction summary below the divider.
8
13
  */
9
- export class CompactionSummaryMessageComponent extends Box {
14
+ export class CompactionSummaryMessageComponent implements Component {
10
15
  #expanded = false;
16
+ #cache?: { width: number; lines: string[] };
17
+ #detail?: Box;
11
18
 
12
- constructor(private readonly message: CompactionSummaryMessage) {
13
- super(1, 1, t => theme.bg("customMessageBg", t));
14
- this.#updateDisplay();
15
- }
19
+ constructor(private readonly message: CompactionSummaryMessage) {}
16
20
 
17
21
  setExpanded(expanded: boolean): void {
22
+ if (this.#expanded === expanded) return;
18
23
  this.#expanded = expanded;
19
- this.#updateDisplay();
24
+ this.#cache = undefined;
20
25
  }
21
26
 
22
- override invalidate(): void {
23
- super.invalidate();
24
- this.#updateDisplay();
27
+ invalidate(): void {
28
+ this.#cache = undefined;
29
+ // Theme may have changed — rebuild the detail box lazily on next render.
30
+ this.#detail = undefined;
25
31
  }
26
32
 
27
- #updateDisplay(): void {
28
- this.clear();
33
+ render(width: number): readonly string[] {
34
+ width = Math.max(1, width);
35
+ if (this.#cache?.width === width) {
36
+ return this.#cache.lines;
37
+ }
38
+ const lines = this.#expanded
39
+ ? ["", this.#divider(width), "", ...this.#detailBox().render(width)]
40
+ : ["", this.#divider(width), ""];
41
+ this.#cache = { width, lines };
42
+ return lines;
43
+ }
29
44
 
30
- const tokenStr = this.message.tokensBefore.toLocaleString();
31
- const label = theme.fg("customMessageLabel", theme.bold("[compaction]"));
32
- this.addChild(new Text(label, 0, 0));
33
- this.addChild(new Spacer(1));
45
+ #divider(width: number): string {
46
+ const rule = theme.tree.horizontal;
47
+ const label = `${theme.icon.camera} compacted`;
48
+ // sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
49
+ const hint = `${theme.sep.dot.trim()} ctrl+o`;
50
+ const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
51
+ // ` label hint ` framed by rules on both sides.
52
+ const remaining = width - plainWidth - 2;
53
+ if (remaining < 4) {
54
+ // Too narrow for a framed rule — emit the bare label.
55
+ return theme.fg("muted", label);
56
+ }
57
+ const left = Math.floor(remaining / 2);
58
+ const right = remaining - left;
59
+ return (
60
+ theme.fg("dim", rule.repeat(left)) +
61
+ ` ${theme.fg("muted", label)} ${theme.fg("dim", hint)} ` +
62
+ theme.fg("dim", rule.repeat(right))
63
+ );
64
+ }
34
65
 
35
- if (this.#expanded) {
36
- const header = `**Compacted from ${tokenStr} tokens**\n\n`;
37
- this.addChild(
38
- new Markdown(header + this.message.summary, 0, 0, getMarkdownTheme(), {
66
+ #detailBox(): Box {
67
+ if (this.#detail) return this.#detail;
68
+ const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
69
+ const tokenStr = this.message.tokensBefore.toLocaleString();
70
+ const frameCount = this.message.images?.length ?? 0;
71
+ const frameNote =
72
+ frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
73
+ box.addChild(
74
+ new Markdown(
75
+ `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`,
76
+ 0,
77
+ 0,
78
+ getMarkdownTheme(),
79
+ {
39
80
  color: (text: string) => theme.fg("customMessageText", text),
40
- }),
41
- );
42
- } else {
43
- this.addChild(
44
- new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (ctrl+o to expand)`), 0, 0),
45
- );
46
- if (this.message.shortSummary) {
47
- this.addChild(new Text(theme.fg("customMessageText", this.message.shortSummary), 0, 1));
48
- }
49
- }
81
+ },
82
+ ),
83
+ );
84
+ this.#detail = box;
85
+ return box;
50
86
  }
51
87
  }
@@ -175,6 +175,8 @@ export class CustomEditor extends Editor {
175
175
  onDequeue?: () => void;
176
176
  /** Called when Caps Lock is pressed. */
177
177
  onCapsLock?: () => void;
178
+ /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
179
+ onLeftAtStart?: () => void;
178
180
 
179
181
  /** Custom key handlers from extensions and non-built-in app actions. */
180
182
  #customKeyHandlers = new Map<KeyId, () => void>();
@@ -257,6 +259,14 @@ export class CustomEditor extends Editor {
257
259
  const parsedKey = parseKey(data);
258
260
  const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
259
261
 
262
+ // Left-arrow on an empty editor: surface for the agent-hub double-tap
263
+ // gesture. Plain "left" only — modified arrows and any in-text cursor
264
+ // movement fall through to normal handling.
265
+ if (canonical === "left" && this.onLeftAtStart && this.getText().trim() === "") {
266
+ this.onLeftAtStart();
267
+ return;
268
+ }
269
+
260
270
  if (canonical !== undefined) {
261
271
  // Intercept configured image paste (async - fires and handles result)
262
272
  if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
@@ -57,6 +57,7 @@ export interface MCPAddWizardOAuthResult {
57
57
  credentialId: string;
58
58
  clientId?: string;
59
59
  clientSecret?: string;
60
+ resource?: string;
60
61
  }
61
62
 
62
63
  interface WizardState {
@@ -71,6 +72,7 @@ interface WizardState {
71
72
  oauthClientId: string;
72
73
  oauthClientSecret: string;
73
74
  oauthScopes: string;
75
+ oauthResource: string;
74
76
  oauthCredentialId: string | null;
75
77
  apiKey: string;
76
78
  authLocation: AuthLocation | null;
@@ -101,6 +103,7 @@ export class MCPAddWizard extends Container {
101
103
  oauthClientId: "",
102
104
  oauthClientSecret: "",
103
105
  oauthScopes: "",
106
+ oauthResource: "",
104
107
  oauthCredentialId: null,
105
108
  apiKey: "",
106
109
  authLocation: null,
@@ -122,6 +125,7 @@ export class MCPAddWizard extends Container {
122
125
  clientId: string,
123
126
  clientSecret: string,
124
127
  scopes: string,
128
+ resource?: string,
125
129
  ) => Promise<MCPAddWizardOAuthResult>)
126
130
  | null = null;
127
131
  #onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
@@ -136,6 +140,7 @@ export class MCPAddWizard extends Container {
136
140
  clientId: string,
137
141
  clientSecret: string,
138
142
  scopes: string,
143
+ resource?: string,
139
144
  ) => Promise<MCPAddWizardOAuthResult>,
140
145
  onTestConnection?: (config: MCPServerConfig) => Promise<void>,
141
146
  onRender?: () => void,
@@ -987,6 +992,7 @@ export class MCPAddWizard extends Container {
987
992
  this.#state.oauthTokenUrl = oauth.tokenUrl;
988
993
  this.#state.oauthClientId = oauth.clientId || "";
989
994
  this.#state.oauthScopes = oauth.scopes || "";
995
+ this.#state.oauthResource = oauth.resource || (this.#state.transport === "stdio" ? "" : this.#state.url);
990
996
  this.#state.authMethod = "oauth";
991
997
 
992
998
  this.#contentContainer.clear();
@@ -1054,6 +1060,7 @@ export class MCPAddWizard extends Container {
1054
1060
  type: "oauth",
1055
1061
  credentialId: this.#state.oauthCredentialId,
1056
1062
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1063
+ resource: this.#state.oauthResource || undefined,
1057
1064
  clientId: this.#state.oauthClientId || undefined,
1058
1065
  clientSecret: this.#state.oauthClientSecret || undefined,
1059
1066
  };
@@ -1081,6 +1088,7 @@ export class MCPAddWizard extends Container {
1081
1088
  type: "oauth",
1082
1089
  credentialId: this.#state.oauthCredentialId,
1083
1090
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1091
+ resource: this.#state.oauthResource || undefined,
1084
1092
  clientId: this.#state.oauthClientId || undefined,
1085
1093
  clientSecret: this.#state.oauthClientSecret || undefined,
1086
1094
  };
@@ -1142,12 +1150,14 @@ export class MCPAddWizard extends Container {
1142
1150
 
1143
1151
  try {
1144
1152
  // Call OAuth handler
1153
+ const oauthResource = this.#state.oauthResource || (this.#state.transport === "stdio" ? "" : this.#state.url);
1145
1154
  const oauthResult = await this.#onOAuthCallback(
1146
1155
  this.#state.oauthAuthUrl,
1147
1156
  this.#state.oauthTokenUrl,
1148
1157
  this.#state.oauthClientId,
1149
1158
  this.#state.oauthClientSecret,
1150
1159
  this.#state.oauthScopes,
1160
+ oauthResource || undefined,
1151
1161
  );
1152
1162
 
1153
1163
  // Store credential ID + any dynamically-registered client credentials,
@@ -1155,6 +1165,7 @@ export class MCPAddWizard extends Container {
1155
1165
  this.#state.oauthCredentialId = oauthResult.credentialId;
1156
1166
  if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
1157
1167
  if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
1168
+ this.#state.oauthResource = oauthResult.resource ?? oauthResource;
1158
1169
 
1159
1170
  // Show success message
1160
1171
  this.#contentContainer.clear();
@@ -1284,6 +1295,7 @@ export class MCPAddWizard extends Container {
1284
1295
  type: "oauth",
1285
1296
  credentialId: this.#state.oauthCredentialId,
1286
1297
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1298
+ resource: this.#state.oauthResource || undefined,
1287
1299
  clientId: this.#state.oauthClientId || undefined,
1288
1300
  clientSecret: this.#state.oauthClientSecret || undefined,
1289
1301
  };
@@ -1312,6 +1324,7 @@ export class MCPAddWizard extends Container {
1312
1324
  type: "oauth",
1313
1325
  credentialId: this.#state.oauthCredentialId,
1314
1326
  tokenUrl: this.#state.oauthTokenUrl || undefined,
1327
+ resource: this.#state.oauthResource || undefined,
1315
1328
  clientId: this.#state.oauthClientId || undefined,
1316
1329
  clientSecret: this.#state.oauthClientSecret || undefined,
1317
1330
  };
@@ -196,6 +196,7 @@ export interface StatusLinePreviewSettings {
196
196
  rightSegments?: StatusLineSegmentId[];
197
197
  separator?: StatusLineSeparatorStyle;
198
198
  sessionAccent?: boolean;
199
+ transparent?: boolean;
199
200
  }
200
201
 
201
202
  export interface SettingsCallbacks {
@@ -590,6 +591,7 @@ export class SettingsSelectorComponent extends Container {
590
591
  rightSegments: settings.get("statusLine.rightSegments"),
591
592
  separator: settings.get("statusLine.separator"),
592
593
  sessionAccent: settings.get("statusLine.sessionAccent"),
594
+ transparent: settings.get("statusLine.transparent"),
593
595
  };
594
596
  this.callbacks.onStatusLinePreview?.(statusLineSettings);
595
597
  this.#updateStatusPreview();
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
4
4
  import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
5
5
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
6
- import { formatCount, getProjectDir } from "@oh-my-pi/pi-utils";
6
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
7
7
  import { $ } from "bun";
8
8
  import { settings } from "../../../config/settings";
9
9
  import type { AgentSession } from "../../../session/agent-session";
@@ -196,6 +196,7 @@ export class StatusLineComponent implements Component {
196
196
  showHookStatus: settings.get("statusLine.showHookStatus"),
197
197
  segmentOptions: settings.getGroup("statusLine").segmentOptions,
198
198
  sessionAccent: settings.get("statusLine.sessionAccent"),
199
+ transparent: settings.get("statusLine.transparent"),
199
200
  };
200
201
  }
201
202
 
@@ -713,7 +714,15 @@ export class StatusLineComponent implements Component {
713
714
  const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions, includeContext);
714
715
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
715
716
 
716
- const bgAnsi = theme.getBgAnsi("statusLineBg");
717
+ // `transparent` reuses the empty-string sentinel (`\x1b[49m`) so the bar
718
+ // inherits the terminal's default background, matching custom themes that
719
+ // set `statusLineBg: ""`. Powerline end caps need a contrasting fill to
720
+ // bridge the bar into the surrounding terminal; without one they read as
721
+ // stray glyphs, so the cap renderer drops them when the fill is empty.
722
+ const TRANSPARENT_BG_ANSI = "\x1b[49m";
723
+ const themeBgAnsi = theme.getBgAnsi("statusLineBg");
724
+ const bgAnsi = effectiveSettings.transparent ? TRANSPARENT_BG_ANSI : themeBgAnsi;
725
+ const transparentBg = bgAnsi === TRANSPARENT_BG_ANSI;
717
726
  const fgAnsi = theme.getFgAnsi("text");
718
727
  const sepAnsi = theme.getFgAnsi("statusLineSep");
719
728
 
@@ -738,9 +747,7 @@ export class StatusLineComponent implements Component {
738
747
 
739
748
  const runningBackgroundJobs = this.session.getAsyncJobSnapshot()?.running.length ?? 0;
740
749
  if (runningBackgroundJobs > 0) {
741
- const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
742
- const label = `${formatCount("job", runningBackgroundJobs)} running`;
743
- rightParts.push(theme.fg("statusLineSubagents", `${icon}${label}`));
750
+ rightParts.unshift(theme.fg("statusLineSubagents", `${theme.icon.job} ${runningBackgroundJobs}`));
744
751
  }
745
752
  const topFillWidth = Math.max(0, width);
746
753
  const left = [...leftParts];
@@ -748,8 +755,10 @@ export class StatusLineComponent implements Component {
748
755
 
749
756
  const leftSepWidth = visibleWidth(separatorDef.left);
750
757
  const rightSepWidth = visibleWidth(separatorDef.right);
751
- const leftCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
752
- const rightCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0;
758
+ // Transparent mode drops powerline caps (they need a bg fill to bridge),
759
+ // so the width budget excludes them too.
760
+ const leftCapWidth = separatorDef.endCaps && !transparentBg ? visibleWidth(separatorDef.endCaps.right) : 0;
761
+ const rightCapWidth = separatorDef.endCaps && !transparentBg ? visibleWidth(separatorDef.endCaps.left) : 0;
753
762
 
754
763
  const groupWidth = (parts: string[], capWidth: number, sepWidth: number): number => {
755
764
  if (parts.length === 0) return 0;
@@ -810,11 +819,12 @@ export class StatusLineComponent implements Component {
810
819
  const renderGroup = (parts: string[], direction: "left" | "right"): string => {
811
820
  if (parts.length === 0) return "";
812
821
  const sep = direction === "left" ? separatorDef.left : separatorDef.right;
813
- const cap = separatorDef.endCaps
814
- ? direction === "left"
815
- ? separatorDef.endCaps.right
816
- : separatorDef.endCaps.left
817
- : "";
822
+ const cap =
823
+ separatorDef.endCaps && !transparentBg
824
+ ? direction === "left"
825
+ ? separatorDef.endCaps.right
826
+ : separatorDef.endCaps.left
827
+ : "";
818
828
  const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
819
829
  const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
820
830
 
@@ -18,6 +18,9 @@ export interface StatusLineSettings {
18
18
  segmentOptions?: StatusLineSegmentOptions;
19
19
  showHookStatus?: boolean;
20
20
  sessionAccent?: boolean;
21
+ /** Drop the theme's `statusLineBg` fill and powerline caps so the bar
22
+ * inherits the terminal's default background. */
23
+ transparent?: boolean;
21
24
  }
22
25
 
23
26
  export type EffectiveStatusLineSettings = Required<
@@ -19,6 +19,7 @@ import type { Theme } from "../../modes/theme/theme";
19
19
  import { theme } from "../../modes/theme/theme";
20
20
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
21
21
  import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
22
+ import { isWaitingPollDetails } from "../../tools/job";
22
23
  import {
23
24
  formatArgsInline,
24
25
  JSON_TREE_MAX_DEPTH_COLLAPSED,
@@ -194,6 +195,11 @@ export class ToolExecutionComponent extends Container {
194
195
  // sealed the block stays in the transcript's repaintable live region so a
195
196
  // late result still repaints instead of stranding the streaming preview.
196
197
  #sealed = false;
198
+ // A `job` poll result whose watched jobs are all still running. Such a
199
+ // block never finalizes (stays in the transcript live region) so a
200
+ // follow-up `job` call can displace it instead of stacking another
201
+ // "waiting on N jobs" frame. Cleared by `seal()`.
202
+ #displaceable = false;
197
203
  #renderState: {
198
204
  spinnerFrame?: number;
199
205
  expanded: boolean;
@@ -359,6 +365,11 @@ export class ToolExecutionComponent extends Container {
359
365
  ): void {
360
366
  this.#result = result;
361
367
  this.#isPartial = isPartial;
368
+ // A `job` poll that found every watched job still running is transient
369
+ // "still waiting" chrome; keep the block displaceable so the next `job`
370
+ // call replaces it instead of stacking another waiting frame (see the
371
+ // event controller's displaceable-poll bookkeeping).
372
+ this.#displaceable = this.#toolName === "job" && result.isError !== true && isWaitingPollDetails(result.details);
362
373
  // When tool is complete, ensure args are marked complete so spinner stops
363
374
  if (!isPartial) {
364
375
  this.#argsComplete = true;
@@ -425,7 +436,11 @@ export class ToolExecutionComponent extends Container {
425
436
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
426
437
  const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
427
438
  const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
428
- const needsSpinner = isStreamingArgs || isPartialTask;
439
+ // A displaceable waiting poll keeps its spinner ticking: it reads as one
440
+ // persistent live poll, and the changing leading glyph keeps the
441
+ // transcript's stable-prefix ratchet from committing rows of a block
442
+ // that a follow-up `job` call may remove.
443
+ const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
429
444
  if (needsSpinner && !this.#spinnerInterval) {
430
445
  const now = performance.now();
431
446
  const frameCount = theme.spinnerFrames.length;
@@ -513,6 +528,9 @@ export class ToolExecutionComponent extends Container {
513
528
  isTranscriptBlockFinalized(): boolean {
514
529
  if (this.#sealed) return true;
515
530
  if (this.#result === undefined) return false;
531
+ // A displaceable waiting poll stays live: its rows are kept out of
532
+ // native scrollback so a follow-up `job` call can remove the block.
533
+ if (this.#displaceable) return false;
516
534
  if (!this.#isPartial) return true;
517
535
  // Partial result: a background async tool is accepted to freeze (the agent
518
536
  // continues while it runs and would otherwise pin an unbounded live region);
@@ -528,11 +546,23 @@ export class ToolExecutionComponent extends Container {
528
546
  seal(): void {
529
547
  if (this.#sealed) return;
530
548
  this.#sealed = true;
549
+ this.#displaceable = false;
531
550
  this.stopAnimation();
532
551
  this.#updateDisplay();
533
552
  this.#ui.requestRender();
534
553
  }
535
554
 
555
+ /**
556
+ * Whether this block is a waiting `job` poll (every watched job still
557
+ * running) that has not been sealed. Such a block never finalized, so none
558
+ * of its rows entered native scrollback (the ticking spinner keeps the
559
+ * stable-prefix ratchet at zero) and the whole block can be removed when a
560
+ * follow-up `job` call supersedes it.
561
+ */
562
+ isDisplaceableBlock(): boolean {
563
+ return this.#displaceable && !this.#sealed;
564
+ }
565
+
536
566
  /**
537
567
  * Stop spinner animation and cleanup resources.
538
568
  */