@dungle-scrubs/tallow 0.8.21 → 0.8.23

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 (217) hide show
  1. package/dist/cli.js +35 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +2 -0
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +82 -0
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/sdk.d.ts +17 -0
  10. package/dist/sdk.d.ts.map +1 -1
  11. package/dist/sdk.js +68 -1
  12. package/dist/sdk.js.map +1 -1
  13. package/dist/workspace-transition-relay.d.ts +40 -7
  14. package/dist/workspace-transition-relay.d.ts.map +1 -1
  15. package/dist/workspace-transition-relay.js +81 -16
  16. package/dist/workspace-transition-relay.js.map +1 -1
  17. package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
  18. package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
  19. package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
  20. package/extensions/_shared/atomic-write.ts +1 -1
  21. package/extensions/_shared/bordered-box.ts +102 -0
  22. package/extensions/_shared/interop-events.ts +5 -0
  23. package/extensions/_shared/pid-registry.ts +1 -1
  24. package/extensions/agent-commands-tool/index.ts +4 -1
  25. package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
  26. package/extensions/background-task-tool/index.ts +139 -221
  27. package/extensions/bash-tool-enhanced/index.ts +1 -75
  28. package/extensions/cd-tool/index.ts +2 -2
  29. package/extensions/context-fork/spawn.ts +4 -1
  30. package/extensions/health/index.ts +6 -6
  31. package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
  32. package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
  33. package/extensions/hooks/index.ts +27 -4
  34. package/extensions/loop/__tests__/loop.test.ts +168 -4
  35. package/extensions/loop/extension.json +6 -5
  36. package/extensions/loop/index.ts +242 -31
  37. package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
  38. package/extensions/plan-mode-tool/index.ts +103 -41
  39. package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
  40. package/extensions/prompt-suggestions/index.ts +41 -6
  41. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
  42. package/extensions/slash-command-bridge/extension.json +1 -1
  43. package/extensions/slash-command-bridge/index.ts +230 -116
  44. package/extensions/subagent-tool/index.ts +2 -2
  45. package/extensions/subagent-tool/process.ts +4 -5
  46. package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
  47. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
  48. package/extensions/teams-tool/dashboard.ts +3 -5
  49. package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
  50. package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
  51. package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
  52. package/extensions/wezterm-pane-control/index.ts +113 -8
  53. package/package.json +6 -4
  54. package/packages/tallow-tui/README.md +51 -0
  55. package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
  56. package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
  57. package/packages/tallow-tui/dist/autocomplete.js +564 -0
  58. package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
  59. package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
  60. package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
  61. package/packages/tallow-tui/dist/border-styles.js +46 -0
  62. package/packages/tallow-tui/dist/border-styles.js.map +1 -0
  63. package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
  64. package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
  65. package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
  66. package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
  67. package/packages/tallow-tui/dist/components/box.d.ts +22 -0
  68. package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
  69. package/packages/tallow-tui/dist/components/box.js +104 -0
  70. package/packages/tallow-tui/dist/components/box.js.map +1 -0
  71. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
  72. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  73. package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
  74. package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
  75. package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
  76. package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
  77. package/packages/tallow-tui/dist/components/editor.js +1766 -0
  78. package/packages/tallow-tui/dist/components/editor.js.map +1 -0
  79. package/packages/tallow-tui/dist/components/image.d.ts +126 -0
  80. package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
  81. package/packages/tallow-tui/dist/components/image.js +245 -0
  82. package/packages/tallow-tui/dist/components/image.js.map +1 -0
  83. package/packages/tallow-tui/dist/components/input.d.ts +37 -0
  84. package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
  85. package/packages/tallow-tui/dist/components/input.js +439 -0
  86. package/packages/tallow-tui/dist/components/input.js.map +1 -0
  87. package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
  88. package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
  89. package/packages/tallow-tui/dist/components/loader.js +146 -0
  90. package/packages/tallow-tui/dist/components/loader.js.map +1 -0
  91. package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
  92. package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
  93. package/packages/tallow-tui/dist/components/markdown.js +633 -0
  94. package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
  95. package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
  96. package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
  97. package/packages/tallow-tui/dist/components/select-list.js +156 -0
  98. package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
  99. package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
  100. package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
  101. package/packages/tallow-tui/dist/components/settings-list.js +189 -0
  102. package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
  103. package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
  104. package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
  105. package/packages/tallow-tui/dist/components/spacer.js +23 -0
  106. package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
  107. package/packages/tallow-tui/dist/components/text.d.ts +19 -0
  108. package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
  109. package/packages/tallow-tui/dist/components/text.js +91 -0
  110. package/packages/tallow-tui/dist/components/text.js.map +1 -0
  111. package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
  112. package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
  113. package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
  114. package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
  115. package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
  116. package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
  117. package/packages/tallow-tui/dist/editor-component.js +2 -0
  118. package/packages/tallow-tui/dist/editor-component.js.map +1 -0
  119. package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
  120. package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
  121. package/packages/tallow-tui/dist/fuzzy.js +107 -0
  122. package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
  123. package/packages/tallow-tui/dist/index.d.ts +25 -0
  124. package/packages/tallow-tui/dist/index.d.ts.map +1 -0
  125. package/packages/tallow-tui/dist/index.js +35 -0
  126. package/packages/tallow-tui/dist/index.js.map +1 -0
  127. package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
  128. package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
  129. package/packages/tallow-tui/dist/keybindings.js +114 -0
  130. package/packages/tallow-tui/dist/keybindings.js.map +1 -0
  131. package/packages/tallow-tui/dist/keys.d.ts +168 -0
  132. package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
  133. package/packages/tallow-tui/dist/keys.js +971 -0
  134. package/packages/tallow-tui/dist/keys.js.map +1 -0
  135. package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
  136. package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
  137. package/packages/tallow-tui/dist/kill-ring.js +44 -0
  138. package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
  139. package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
  140. package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
  141. package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
  142. package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
  143. package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
  144. package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
  145. package/packages/tallow-tui/dist/terminal-image.js +460 -0
  146. package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
  147. package/packages/tallow-tui/dist/terminal.d.ts +102 -0
  148. package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
  149. package/packages/tallow-tui/dist/terminal.js +263 -0
  150. package/packages/tallow-tui/dist/terminal.js.map +1 -0
  151. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
  152. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
  153. package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
  154. package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
  155. package/packages/tallow-tui/dist/tui.d.ts +239 -0
  156. package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
  157. package/packages/tallow-tui/dist/tui.js +1058 -0
  158. package/packages/tallow-tui/dist/tui.js.map +1 -0
  159. package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
  160. package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
  161. package/packages/tallow-tui/dist/undo-stack.js +25 -0
  162. package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
  163. package/packages/tallow-tui/dist/utils.d.ts +96 -0
  164. package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
  165. package/packages/tallow-tui/dist/utils.js +843 -0
  166. package/packages/tallow-tui/dist/utils.js.map +1 -0
  167. package/packages/tallow-tui/package.json +24 -0
  168. package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
  169. package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
  170. package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
  171. package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
  172. package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
  173. package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
  174. package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
  175. package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
  176. package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
  177. package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
  178. package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
  179. package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
  180. package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
  181. package/packages/tallow-tui/src/autocomplete.ts +716 -0
  182. package/packages/tallow-tui/src/border-styles.ts +60 -0
  183. package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
  184. package/packages/tallow-tui/src/components/box.ts +137 -0
  185. package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
  186. package/packages/tallow-tui/src/components/editor.ts +2143 -0
  187. package/packages/tallow-tui/src/components/image.ts +315 -0
  188. package/packages/tallow-tui/src/components/input.ts +522 -0
  189. package/packages/tallow-tui/src/components/loader.ts +187 -0
  190. package/packages/tallow-tui/src/components/markdown.ts +780 -0
  191. package/packages/tallow-tui/src/components/select-list.ts +197 -0
  192. package/packages/tallow-tui/src/components/settings-list.ts +264 -0
  193. package/packages/tallow-tui/src/components/spacer.ts +28 -0
  194. package/packages/tallow-tui/src/components/text.ts +113 -0
  195. package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
  196. package/packages/tallow-tui/src/editor-component.ts +92 -0
  197. package/packages/tallow-tui/src/fuzzy.ts +133 -0
  198. package/packages/tallow-tui/src/index.ts +118 -0
  199. package/packages/tallow-tui/src/keybindings.ts +183 -0
  200. package/packages/tallow-tui/src/keys.ts +1189 -0
  201. package/packages/tallow-tui/src/kill-ring.ts +46 -0
  202. package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
  203. package/packages/tallow-tui/src/terminal-image.ts +619 -0
  204. package/packages/tallow-tui/src/terminal.ts +350 -0
  205. package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
  206. package/packages/tallow-tui/src/tui.ts +1336 -0
  207. package/packages/tallow-tui/src/undo-stack.ts +28 -0
  208. package/packages/tallow-tui/src/utils.ts +948 -0
  209. package/packages/tallow-tui/tsconfig.build.json +21 -0
  210. package/runtime/agent-runner.ts +20 -0
  211. package/runtime/atomic-write.ts +8 -0
  212. package/runtime/otel.ts +12 -0
  213. package/runtime/resolve-module.ts +23 -0
  214. package/runtime/runtime-path-provider.ts +12 -0
  215. package/runtime/runtime-provenance.ts +17 -0
  216. package/runtime/workspace-transition-relay.ts +21 -0
  217. package/runtime/workspace-transition.ts +29 -0
@@ -8,7 +8,7 @@
8
8
  * - `task_output` tool: Retrieve output from a background task
9
9
  * - `task_status` tool: Check if a task is running or completed
10
10
  * - `/bg` command: List and manage background tasks
11
- * - Status widget shows running background tasks
11
+ * - Live widget shows running background tasks when the tasks extension is not presenting them
12
12
  *
13
13
  * Usage:
14
14
  * Ask the agent to "run npm test in the background"
@@ -25,9 +25,7 @@ import {
25
25
  type Theme,
26
26
  } from "@mariozechner/pi-coding-agent";
27
27
  import {
28
- Container,
29
28
  Key,
30
- Loader,
31
29
  matchesKey,
32
30
  Text,
33
31
  type TUI,
@@ -35,7 +33,7 @@ import {
35
33
  visibleWidth,
36
34
  } from "@mariozechner/pi-tui";
37
35
  import { Type } from "@sinclair/typebox";
38
- import { getIcon, getSpinner } from "../_icons/index.js";
36
+ import { getIcon } from "../_icons/index.js";
39
37
  import { extractPreview, isInlineResultsEnabled } from "../_shared/inline-preview.js";
40
38
  import {
41
39
  emitInteropEvent,
@@ -43,6 +41,7 @@ import {
43
41
  INTEROP_EVENT_NAMES,
44
42
  type InteropBackgroundTaskView,
45
43
  onInteropEvent,
44
+ requestInteropState,
46
45
  } from "../_shared/interop-events.js";
47
46
  import { registerPid, unregisterPid } from "../_shared/pid-registry.js";
48
47
  import {
@@ -128,15 +127,14 @@ let piRef: ExtensionAPI | null = null;
128
127
  */
129
128
  const promotedAbortControllers = new Map<string, AbortController>();
130
129
 
131
- // TUI reference captured at session_start for Loader in renderResult
130
+ // TUI reference captured at session_start for widgets
132
131
  let tuiRef: TUI | null = null;
133
132
 
134
- // Persistent Loader instances per streaming task (avoids leaking intervals)
135
- const activeLoaders = new Map<string, InstanceType<typeof Loader>>();
136
-
137
133
  // In-memory task registry (published via typed interop events)
138
134
  const tasks = new Map<string, BackgroundTask>();
139
135
  let taskCounter = 0;
136
+ let backgroundTaskPresenterActive = false;
137
+ let interopPresenterStateCleanup: (() => void) | undefined;
140
138
  let interopStateRequestCleanup: (() => void) | undefined;
141
139
 
142
140
  /** Shared mutable state for live-updating anchor components during consecutive polls. */
@@ -465,7 +463,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
465
463
  piEventsRef = pi.events;
466
464
  piRef = pi;
467
465
 
468
- // Register inline result renderer for fire-and-forget task completions
466
+ // Register inline result renderer for background task completions
469
467
  pi.registerMessageRenderer<BgTaskCompleteDetails>(
470
468
  "background-task-complete",
471
469
  (message, _options, theme) => {
@@ -511,10 +509,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
511
509
  );
512
510
 
513
511
  /**
514
- * Post an inline notification when a fire-and-forget task completes.
515
- *
512
+ * Post an inline notification when a background task completes.
516
513
  * Checks the inlineAgentResults setting before posting.
517
- * Only fires for fire-and-forget tasks (background: true).
518
514
  *
519
515
  * @param task - Completed background task
520
516
  * @returns void
@@ -544,12 +540,36 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
544
540
  });
545
541
  }
546
542
 
543
+ /** Debounce timer for widget updates during rapid output. */
544
+ let widgetDebounceTimer: ReturnType<typeof setTimeout> | null = null;
545
+
546
+ /** Minimum interval between widget re-renders (milliseconds). */
547
+ const WIDGET_DEBOUNCE_MS = 150;
548
+
549
+ /** Maximum tail lines per task in the widget. */
550
+ const WIDGET_TAIL_LINES = 3;
551
+
552
+ /** Most recent UI context used for widget/status updates. */
553
+ let lastWidgetContext: ExtensionContext | null = null;
554
+
555
+ /**
556
+ * Whether the tasks extension currently owns background-task presentation.
557
+ *
558
+ * When true, background-task-tool suppresses its duplicate live widget and
559
+ * lets the tasks widget render the canonical summary row.
560
+ *
561
+ * @returns True when the tasks widget should be the only presenter
562
+ */
563
+ function shouldSuppressTaskWidget(): boolean {
564
+ return backgroundTaskPresenterActive;
565
+ }
566
+
547
567
  /**
548
568
  * Updates the status bar indicator for running background tasks.
569
+ *
549
570
  * @param ctx - Extension context for UI access
550
571
  */
551
- function updateWidget(ctx: ExtensionContext): void {
552
- // Guard: ctx.ui may be undefined if context is stale (e.g., from async callback after shutdown)
572
+ function updateStatusBar(ctx: ExtensionContext): void {
553
573
  if (!ctx?.ui) return;
554
574
 
555
575
  const running = [...tasks.values()].filter((t) => t.status === "running");
@@ -559,10 +579,62 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
559
579
  return;
560
580
  }
561
581
 
562
- // Status bar only - widget is rendered by tasks extension
563
582
  ctx.ui.setStatus("bg-tasks", `${FG_PURPLE}⚙ ${running.length} bg${RESET_ALL}`);
564
583
  }
565
584
 
585
+ /**
586
+ * Render the bottom widget showing live output from running background tasks.
587
+ * Shows each running task with a compact command preview and tail output.
588
+ * Placed above the editor so async work is visible without cluttering the chat.
589
+ *
590
+ * @param ctx - Extension context for widget access
591
+ */
592
+ function updateTaskWidget(ctx: ExtensionContext): void {
593
+ if (!ctx?.ui) return;
594
+
595
+ const running = [...tasks.values()].filter((t) => t.status === "running");
596
+
597
+ if (running.length === 0 || shouldSuppressTaskWidget()) {
598
+ ctx.ui.setWidget("bg-tasks", undefined);
599
+ return;
600
+ }
601
+
602
+ const lines: string[] = [];
603
+ for (const task of running) {
604
+ const elapsed = formatDuration(Date.now() - task.startTime);
605
+ const cmd = truncateCommand(task.command, 50);
606
+ const lineCount = task.output.join("").split("\n").length;
607
+ const header = `${FG_PURPLE}⚙${RESET_ALL} ${FG_WHITE}${cmd}${RESET_ALL} ${FG_PURPLE_MUTED}(${elapsed}, ${lineCount} lines)${RESET_ALL}`;
608
+ lines.push(header);
609
+
610
+ // Show tail output lines
611
+ const fullOutput = task.output.join("");
612
+ if (fullOutput.length > 0) {
613
+ const outputLines = fullOutput.split("\n").filter((l) => l.trim().length > 0);
614
+ const tail = outputLines.slice(-WIDGET_TAIL_LINES);
615
+ for (const line of tail) {
616
+ const clean = line.length > 80 ? `${line.slice(0, 79)}…` : line;
617
+ lines.push(` ${FG_PURPLE_MUTED}│${RESET_ALL} ${FG_PURPLE_MUTED}${clean}${RESET_ALL}`);
618
+ }
619
+ }
620
+ }
621
+
622
+ ctx.ui.setWidget("bg-tasks", lines, { placement: "aboveEditor" });
623
+ }
624
+
625
+ /**
626
+ * Debounced widget update — called from onData to avoid thrashing renders.
627
+ *
628
+ * @param ctx - Extension context for widget access
629
+ */
630
+ function debouncedWidgetUpdate(ctx: ExtensionContext): void {
631
+ if (widgetDebounceTimer) return;
632
+ widgetDebounceTimer = setTimeout(() => {
633
+ widgetDebounceTimer = null;
634
+ updateTaskWidget(ctx);
635
+ }, WIDGET_DEBOUNCE_MS);
636
+ }
637
+
566
638
  /**
567
639
  * Sync background-task state to both local UI and cross-extension event consumers.
568
640
  *
@@ -570,10 +642,25 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
570
642
  * @returns void
571
643
  */
572
644
  function syncTaskState(ctx: ExtensionContext): void {
573
- updateWidget(ctx);
645
+ lastWidgetContext = ctx;
646
+ updateStatusBar(ctx);
647
+ updateTaskWidget(ctx);
574
648
  publishBackgroundTaskSnapshot(pi.events);
575
649
  }
576
650
 
651
+ interopPresenterStateCleanup?.();
652
+ interopPresenterStateCleanup = onInteropEvent(
653
+ pi.events,
654
+ INTEROP_EVENT_NAMES.backgroundTasksPresenterState,
655
+ (payload) => {
656
+ backgroundTaskPresenterActive = payload.active;
657
+ const currentContext = lastWidgetContext;
658
+ if (currentContext?.ui) {
659
+ updateTaskWidget(currentContext);
660
+ }
661
+ }
662
+ );
663
+
577
664
  interopStateRequestCleanup?.();
578
665
  interopStateRequestCleanup = onInteropEvent(pi.events, INTEROP_EVENT_NAMES.stateRequest, () => {
579
666
  publishBackgroundTaskSnapshot(pi.events);
@@ -608,7 +695,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
608
695
  name: "bg_bash",
609
696
  label: "bg_bash",
610
697
  description:
611
- "Run a bash command in the background. By default, streams live output and waits for completion. Set background=true for fire-and-forget daemons/servers.\n\nWHEN TO USE:\n- Starting daemons or servers (with background: true)\n- Long-running builds or tests (default: streams output)\n- Any process you want to run independently\n\nWARNING: Never use bash tool with & to background processes - it will hang. Use bg_bash instead.",
698
+ "Run a bash command in the background. Returns immediately without blocking. When the tasks extension is active, progress appears in the Background Tasks widget; otherwise background-task-tool shows its own live widget.\n\nWHEN TO USE:\n- Starting daemons or servers\n- Long-running builds or tests\n- Any process you want to run independently\n\nThe command runs asynchronously. Use task_status/task_output to check results.\n\nWARNING: Never use bash tool with & to background processes - it will hang. Use bg_bash instead.",
612
699
  parameters: Type.Object({
613
700
  command: Type.String({
614
701
  description: "Bash command to run in background",
@@ -620,15 +707,13 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
620
707
  ),
621
708
  background: Type.Optional(
622
709
  Type.Boolean({
623
- description:
624
- "If true, return immediately without streaming output. Use for daemons/servers.",
710
+ description: "Deprecated — bg_bash always runs in background. Ignored.",
625
711
  })
626
712
  ),
627
713
  }),
628
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
714
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
629
715
  const taskId = generateTaskId();
630
716
  const cwd = ctx.cwd;
631
- const fireAndForget = params.background === true;
632
717
 
633
718
  const task: BackgroundTask = {
634
719
  id: taskId,
@@ -658,7 +743,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
658
743
  registerPid(child.pid, params.command);
659
744
  }
660
745
 
661
- // Buffer output (and stream if not fire-and-forget)
746
+ // Buffer output and update widget (never blocks the agent)
662
747
  const onData = (data: Buffer) => {
663
748
  // Guard: ignore data arriving after task is no longer running
664
749
  if (task.status !== "running") return;
@@ -673,14 +758,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
673
758
  }
674
759
  }
675
760
 
676
- // Stream live updates to the TUI
677
- if (!fireAndForget) {
678
- const output = task.output.join("");
679
- onUpdate?.({
680
- content: [{ type: "text", text: output || "(no output yet)" }],
681
- details: { taskId },
682
- });
683
- }
761
+ // Update the bottom widget with live output (debounced)
762
+ debouncedWidgetUpdate(ctx);
684
763
  };
685
764
 
686
765
  const lifecycle = createProcessLifecycle({
@@ -702,7 +781,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
702
781
  task.process = null;
703
782
  syncTaskState(ctx);
704
783
  },
705
- signal: fireAndForget ? undefined : signal,
784
+ signal: undefined,
706
785
  timeoutMs: params.timeout && params.timeout > 0 ? params.timeout * 1000 : undefined,
707
786
  });
708
787
 
@@ -746,65 +825,25 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
746
825
  syncTaskState(ctx);
747
826
  };
748
827
 
749
- // Fire-and-forget: return immediately
750
- if (fireAndForget) {
751
- void lifecycle.waitForExit().then((result) => {
752
- applyLifecycleResult({
753
- ...(result.type === "close" ? { code: result.code } : {}),
754
- ...(result.type === "error" ? { error: result.error } : {}),
755
- type: result.type,
756
- });
757
- postInlineResult(task);
828
+ // Always non-blocking: return immediately, process runs in background
829
+ void lifecycle.waitForExit().then((result) => {
830
+ applyLifecycleResult({
831
+ ...(result.type === "close" ? { code: result.code } : {}),
832
+ ...(result.type === "error" ? { error: result.error } : {}),
833
+ type: result.type,
758
834
  });
759
- lifecycle.detach();
760
- syncTaskState(ctx);
761
- lastCompletedPoll = null;
762
-
763
- return {
764
- details: { taskId, command: params.command, fireAndForget: true },
765
- content: [
766
- {
767
- type: "text",
768
- text: `Background task started (fire-and-forget).\nTask ID: ${taskId}\nCommand: ${params.command}\nUse task_output("${taskId}") to check later.`,
769
- },
770
- ],
771
- };
772
- }
773
-
774
- // Streaming mode: wait for process to complete
775
- syncTaskState(ctx);
776
-
777
- const lifecycleResult = await lifecycle.waitForExit();
778
- applyLifecycleResult({
779
- ...(lifecycleResult.type === "close" ? { code: lifecycleResult.code } : {}),
780
- ...(lifecycleResult.type === "error" ? { error: lifecycleResult.error } : {}),
781
- type: lifecycleResult.type,
835
+ postInlineResult(task);
782
836
  });
783
-
784
- // Stop and clean up the persistent Loader for this task
785
- const loader = activeLoaders.get(taskId);
786
- if (loader) {
787
- loader.stop();
788
- activeLoaders.delete(taskId);
789
- }
790
-
791
- const output = task.output.join("");
792
- const duration = formatDuration((task.endTime || Date.now()) - task.startTime);
837
+ lifecycle.detach();
838
+ syncTaskState(ctx);
793
839
  lastCompletedPoll = null;
794
840
 
795
841
  return {
796
- details: {
797
- taskId,
798
- command: params.command,
799
- status: task.status,
800
- duration,
801
- exitCode: task.exitCode,
802
- output,
803
- },
842
+ details: { taskId, command: params.command },
804
843
  content: [
805
844
  {
806
845
  type: "text",
807
- text: output || "(no output)",
846
+ text: `Background task started.\nTask ID: ${taskId}\nCommand: ${params.command}\nUse task_status("${taskId}") to check progress.\nUse task_output("${taskId}") to retrieve output.`,
808
847
  },
809
848
  ],
810
849
  };
@@ -812,149 +851,22 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
812
851
 
813
852
  renderCall(args, theme) {
814
853
  const cmd = truncateCommand(args.command as string, 60);
815
- const bg = args.background ? formatPresentationText(theme, "meta", " (detached)") : "";
816
854
  return new Text(
817
855
  formatPresentationText(theme, "title", "bg_bash ") +
818
- formatPresentationText(theme, "action", cmd) +
819
- bg,
856
+ formatPresentationText(theme, "action", cmd),
820
857
  0,
821
858
  0
822
859
  );
823
860
  },
824
861
 
825
- renderResult(result, { expanded, isPartial }, theme) {
826
- const details = result.details as
827
- | {
828
- fireAndForget?: boolean;
829
- taskId?: string;
830
- status?: string;
831
- duration?: string;
832
- exitCode?: number | null;
833
- }
834
- | undefined;
835
-
836
- // Fire-and-forget: compact one-liner
837
- if (details?.fireAndForget) {
838
- return renderLines([
839
- formatPresentationText(theme, "status_success", "⚙ Started (detached)"),
840
- ]);
841
- }
842
-
843
- const COLLAPSED_LINES = 10;
844
- const EXPANDED_LINES = 50;
845
-
846
- // Extract output (available during streaming via onUpdate and after completion)
847
- const text = result.content[0];
848
- const output = text?.type === "text" ? text.text : "";
849
-
850
- // While running: show streamed output + loader spinner at bottom
851
- if (isPartial) {
852
- const container = new Container();
853
-
854
- if (output) {
855
- const allLines = output.split("\n").filter((l: string) => l.length > 0);
856
- const maxLines = COLLAPSED_LINES;
857
- const truncated = allLines.length > maxLines;
858
- const tail = truncated ? allLines.slice(-maxLines) : allLines;
859
- const lines: string[] = [];
860
- if (truncated) {
861
- appendSection(lines, [
862
- formatPresentationText(
863
- theme,
864
- "meta",
865
- `... ${allLines.length - maxLines} more lines above`
866
- ),
867
- ]);
868
- }
869
- appendSection(
870
- lines,
871
- tail.map((line) => styleBackgroundOutputLine(theme, line))
872
- );
873
- container.addChild(renderLines(lines, { wrap: true }));
874
- }
875
-
876
- // Reuse persistent Loader (one per task, avoids leaking intervals)
877
- const tid = details?.taskId ?? "__bg_default";
878
- let loader = activeLoaders.get(tid);
879
- if (!loader && tuiRef) {
880
- loader = new Loader(
881
- tuiRef,
882
- (s) => theme.fg("warning", s),
883
- (s) => theme.fg("muted", s),
884
- "Running..."
885
- );
886
- (loader as unknown as Record<string, string[]>).frames = getSpinner();
887
- activeLoaders.set(tid, loader);
888
- }
889
- if (loader) {
890
- container.addChild(loader);
891
- } else {
892
- container.addChild(
893
- new Text(formatPresentationText(theme, "status_warning", "Running..."), 0, 0)
894
- );
895
- }
896
-
897
- return container;
898
- }
899
-
900
- // Completed: show output
901
- if (!output) return renderLines([formatPresentationText(theme, "meta", "(no output)")]);
902
-
903
- const allLines = output.split("\n").filter((l: string) => l.length > 0);
904
- const maxLines = expanded ? EXPANDED_LINES : COLLAPSED_LINES;
905
- const truncated = allLines.length > maxLines;
906
- const tail = truncated ? allLines.slice(-maxLines) : allLines;
907
- const lines: string[] = [];
908
-
909
- if (truncated) {
910
- appendSection(lines, [
911
- formatPresentationText(
912
- theme,
913
- "meta",
914
- `... ${allLines.length - maxLines} more lines above`
915
- ),
916
- ]);
917
- }
918
- appendSection(
919
- lines,
920
- tail.map((line) => styleBackgroundOutputLine(theme, line))
921
- );
922
- if (truncated && !expanded) {
923
- appendSection(lines, [
924
- `${formatPresentationText(theme, "meta", `... ${allLines.length - maxLines} more lines`)} ${formatPresentationText(theme, "hint", keyHint("expandTools", "to expand"))}`,
925
- ]);
926
- }
927
-
928
- if (details?.status) {
929
- const statusIcon =
930
- details.status === "completed"
931
- ? getIcon("success")
932
- : details.status === "running"
933
- ? getIcon("in_progress")
934
- : getIcon("error");
935
- const statusRole =
936
- details.status === "completed"
937
- ? "status_success"
938
- : details.status === "running"
939
- ? "status_warning"
940
- : "status_error";
941
- const statusMetaParts: string[] = [];
942
- if (details.exitCode !== null && details.exitCode !== undefined) {
943
- statusMetaParts.push(`exit ${details.exitCode}`);
944
- }
945
- if (details.duration) statusMetaParts.push(details.duration);
946
- const statusMeta = statusMetaParts.length > 0 ? ` (${statusMetaParts.join(", ")})` : "";
947
- appendSection(
948
- lines,
949
- [
950
- formatPresentationText(theme, statusRole, `${statusIcon} bg_bash ${details.status}`) +
951
- formatPresentationText(theme, "meta", statusMeta),
952
- ],
953
- { blankBefore: true }
954
- );
955
- }
956
-
957
- return renderLines(lines, { wrap: expanded });
862
+ renderResult(result, _opts, theme) {
863
+ // bg_bash always returns immediately — compact one-liner
864
+ const details = result.details as { taskId?: string; command?: string } | undefined;
865
+ const taskId = details?.taskId ?? "?";
866
+ return renderLines([
867
+ formatPresentationText(theme, "status_success", `⚙ Started`) +
868
+ formatPresentationText(theme, "meta", ` → task_output("${taskId}")`),
869
+ ]);
958
870
  },
959
871
  });
960
872
 
@@ -1807,23 +1719,29 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
1807
1719
  promotedAbortControllers.clear();
1808
1720
  pollStates.clear();
1809
1721
  lastCompletedPoll = null;
1722
+ backgroundTaskPresenterActive = false;
1723
+ lastWidgetContext = null;
1810
1724
  publishBackgroundTaskSnapshot(pi.events);
1725
+ interopPresenterStateCleanup?.();
1726
+ interopPresenterStateCleanup = undefined;
1811
1727
  interopStateRequestCleanup?.();
1812
1728
  interopStateRequestCleanup = undefined;
1813
1729
  });
1814
1730
 
1815
- // Capture TUI reference and update status on session start
1731
+ // Capture TUI reference and initialize on session start
1816
1732
  pi.on("session_start", async (_event, ctx) => {
1817
- // Capture TUI via a throwaway widget so Loader can be used in renderResult
1733
+ backgroundTaskPresenterActive = false;
1734
+
1735
+ // Capture TUI via a throwaway widget for requestRender access
1818
1736
  ctx.ui.setWidget("bg-tasks-tui-capture", (tui, _theme) => {
1819
1737
  tuiRef = tui;
1820
1738
  return { render: () => [], invalidate: () => {} };
1821
1739
  });
1822
- // Immediately remove — we just needed the reference
1823
1740
  ctx.ui.setWidget("bg-tasks-tui-capture", undefined);
1824
1741
 
1825
1742
  ctx.ui.setStatus("bg-tasks", undefined);
1826
1743
  syncTaskState(ctx);
1744
+ requestInteropState(pi.events, "background-task-tool");
1827
1745
  });
1828
1746
 
1829
1747
  // Register Ctrl+Shift+B shortcut for background tasks
@@ -185,15 +185,6 @@ function handleBashError(err: unknown): AgentToolResult<BashToolDetails | undefi
185
185
  throw err;
186
186
  }
187
187
 
188
- /** Maximum number of output tail lines shown in the working message. */
189
- const PROGRESS_TAIL_LINES = 3;
190
-
191
- /** Maximum character width per progress line before truncation. */
192
- const PROGRESS_LINE_WIDTH = 60;
193
-
194
- /** Minimum interval between working message updates (milliseconds). */
195
- const PROGRESS_DEBOUNCE_MS = 100;
196
-
197
188
  /** Visual prefix for output lines in the progress message. */
198
189
  const PROGRESS_LINE_PREFIX = "│ ";
199
190
 
@@ -380,75 +371,14 @@ export default function bashLive(pi: ExtensionAPI): void {
380
371
  }
381
372
 
382
373
  const cmd = params.command ?? "";
383
- const firstLine = cmd.split("\n")[0];
384
- const preview = firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine;
385
- ctx.ui.setWorkingMessage(`Bash: ${preview}`);
386
-
387
- // Progress: surface output tail in the working message area
388
- let lastProgressTime = 0;
389
- let progressTimeout: ReturnType<typeof setTimeout> | null = null;
390
-
391
- /**
392
- * Debounced update of the working message with the latest output tail.
393
- *
394
- * @param text - Full output text so far
395
- */
396
- const updateProgress = (text: string): void => {
397
- const now = Date.now();
398
- const doUpdate = (): void => {
399
- lastProgressTime = Date.now();
400
- const tail = extractTailLines(text, PROGRESS_TAIL_LINES);
401
- ctx.ui.setWorkingMessage(formatProgressMessage(preview, tail, PROGRESS_LINE_WIDTH));
402
- };
403
- if (now - lastProgressTime >= PROGRESS_DEBOUNCE_MS) {
404
- if (progressTimeout) {
405
- clearTimeout(progressTimeout);
406
- progressTimeout = null;
407
- }
408
- doUpdate();
409
- } else if (!progressTimeout) {
410
- progressTimeout = setTimeout(
411
- () => {
412
- progressTimeout = null;
413
- doUpdate();
414
- },
415
- PROGRESS_DEBOUNCE_MS - (now - lastProgressTime)
416
- );
417
- }
418
- };
419
-
420
- /** Clear any pending progress update timeout. */
421
- const clearProgressTimeout = (): void => {
422
- if (progressTimeout) {
423
- clearTimeout(progressTimeout);
424
- progressTimeout = null;
425
- }
426
- };
427
-
428
- /**
429
- * Wraps onUpdate to surface output progress in the working message.
430
- *
431
- * @param partialResult - Partial tool result from bash execution
432
- */
433
- const progressOnUpdate: typeof onUpdate = (partialResult) => {
434
- onUpdate?.(partialResult);
435
- const text = partialResult?.content?.find((c: { type: string }) => c.type === "text") as
436
- | { text: string }
437
- | undefined;
438
- if (text?.text) updateProgress(text.text);
439
- };
440
-
441
374
  const autoTimeout = readAutoBackgroundTimeout();
442
375
 
443
376
  // Fast path: auto-background disabled or user-provided timeout shorter
444
377
  if (autoTimeout <= 0 || (params.timeout && params.timeout * 1000 <= autoTimeout)) {
445
378
  try {
446
- return await scopedBashTool.execute(toolCallId, params, signal, progressOnUpdate);
379
+ return await scopedBashTool.execute(toolCallId, params, signal, onUpdate);
447
380
  } catch (err) {
448
381
  return handleBashError(err);
449
- } finally {
450
- clearProgressTimeout();
451
- ctx.ui.setWorkingMessage();
452
382
  }
453
383
  }
454
384
 
@@ -481,8 +411,6 @@ export default function bashLive(pi: ExtensionAPI): void {
481
411
  if (text?.text) promotedHandle.replaceOutput(text.text);
482
412
  } else {
483
413
  onUpdate?.(partialResult);
484
- // Update progress message while still in foreground
485
- if (text?.text) updateProgress(text.text);
486
414
  }
487
415
  };
488
416
 
@@ -503,8 +431,6 @@ export default function bashLive(pi: ExtensionAPI): void {
503
431
  });
504
432
 
505
433
  const winner = await Promise.race([bashPromise, timeoutPromise]);
506
- clearProgressTimeout();
507
- ctx.ui.setWorkingMessage();
508
434
 
509
435
  if (winner.type === "completed") {
510
436
  return winner.result;
@@ -14,11 +14,11 @@ import type {
14
14
  ExtensionContext,
15
15
  } from "@mariozechner/pi-coding-agent";
16
16
  import { Type } from "@sinclair/typebox";
17
- import { getWorkspaceTransitionHost } from "../../src/workspace-transition.js";
17
+ import { getWorkspaceTransitionHost } from "../../runtime/workspace-transition.js";
18
18
  import {
19
19
  getRelaySocketPath,
20
20
  requestTransitionViaRelay,
21
- } from "../../src/workspace-transition-relay.js";
21
+ } from "../../runtime/workspace-transition-relay.js";
22
22
  import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
23
23
 
24
24
  /** Details returned from the cd tool. */
@@ -9,7 +9,10 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
- import { DEFAULT_AGENT_RUNNER_ENV, spawnWithResolvedAgentRunner } from "../../src/agent-runner.js";
12
+ import {
13
+ DEFAULT_AGENT_RUNNER_ENV,
14
+ spawnWithResolvedAgentRunner,
15
+ } from "../../runtime/agent-runner.js";
13
16
  import { expandFileReferences } from "../file-reference/index.js";
14
17
 
15
18
  /** Configuration for spawning a forked subprocess. */