@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5

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 (347) hide show
  1. package/CHANGELOG.md +1654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1533 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +199 -0
  43. package/dist/history/ingest.js +468 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +466 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/bridge.js +750 -0
  53. package/dist/pi-intercom/broker/broker.js +426 -0
  54. package/dist/pi-intercom/broker/client.js +492 -0
  55. package/dist/pi-intercom/broker/framing.js +65 -0
  56. package/dist/pi-intercom/broker/paths.js +42 -0
  57. package/dist/pi-intercom/broker/pid.js +66 -0
  58. package/dist/pi-intercom/broker/spawn.js +221 -0
  59. package/dist/pi-intercom/config.js +102 -0
  60. package/dist/pi-intercom/index.js +2942 -0
  61. package/dist/pi-intercom/reply-tracker.js +182 -0
  62. package/dist/pi-intercom/session-targets.js +128 -0
  63. package/dist/pi-intercom/subagent-live-events.js +132 -0
  64. package/dist/pi-intercom/topics.js +157 -0
  65. package/dist/pi-intercom/types.js +163 -0
  66. package/dist/pi-intercom/ui/compose.js +269 -0
  67. package/dist/pi-intercom/ui/inline-message.js +80 -0
  68. package/dist/pi-intercom/ui/session-list.js +124 -0
  69. package/dist/runs/background/async-control.js +95 -0
  70. package/dist/runs/background/async-execution.js +676 -0
  71. package/dist/runs/background/async-job-tracker.js +383 -0
  72. package/dist/runs/background/async-result-file.js +58 -0
  73. package/dist/runs/background/async-resume.js +305 -0
  74. package/dist/runs/background/async-status.js +442 -0
  75. package/dist/runs/background/completion-dedupe.js +69 -0
  76. package/dist/runs/background/completion-delivery.js +245 -0
  77. package/dist/runs/background/notify.js +71 -0
  78. package/dist/runs/background/parallel-groups.js +55 -0
  79. package/dist/runs/background/result-watcher.js +426 -0
  80. package/dist/runs/background/run-id-resolver.js +64 -0
  81. package/dist/runs/background/run-status.js +382 -0
  82. package/dist/runs/background/stale-run-reconciler.js +335 -0
  83. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  84. package/dist/runs/background/subagent-runner.js +1872 -0
  85. package/dist/runs/background/top-level-async.js +5 -0
  86. package/dist/runs/foreground/chain-clarify.js +1152 -0
  87. package/dist/runs/foreground/clarify-invocation.js +72 -0
  88. package/dist/runs/foreground/execution-input.js +423 -0
  89. package/dist/runs/foreground/foreground-control.js +817 -0
  90. package/dist/runs/foreground/question-control.js +88 -0
  91. package/dist/runs/foreground/run-async-path.js +205 -0
  92. package/dist/runs/foreground/subagent-executor.js +442 -0
  93. package/dist/runs/foreground/subagent-params.js +165 -0
  94. package/dist/runs/foreground/wait-run.js +141 -0
  95. package/dist/runs/shared/acceptance-contract.js +253 -0
  96. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  97. package/dist/runs/shared/acceptance-finalization.js +280 -0
  98. package/dist/runs/shared/acceptance-reports.js +147 -0
  99. package/dist/runs/shared/acceptance.js +4 -0
  100. package/dist/runs/shared/chain-outputs.js +110 -0
  101. package/dist/runs/shared/child-attempt.js +693 -0
  102. package/dist/runs/shared/child-execution-cwd.js +96 -0
  103. package/dist/runs/shared/child-observations.js +114 -0
  104. package/dist/runs/shared/claude-code.js +199 -0
  105. package/dist/runs/shared/completion-guard.js +31 -0
  106. package/dist/runs/shared/dynamic-fanout.js +274 -0
  107. package/dist/runs/shared/history-index.js +72 -0
  108. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  109. package/dist/runs/shared/model-fallback.js +185 -0
  110. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  111. package/dist/runs/shared/native-finalization.js +101 -0
  112. package/dist/runs/shared/native-usage.js +153 -0
  113. package/dist/runs/shared/nested-events.js +700 -0
  114. package/dist/runs/shared/nested-path.js +48 -0
  115. package/dist/runs/shared/nested-render.js +108 -0
  116. package/dist/runs/shared/parallel-utils.js +61 -0
  117. package/dist/runs/shared/parent-receipts.js +68 -0
  118. package/dist/runs/shared/parent-usage.js +121 -0
  119. package/dist/runs/shared/pi-args.js +310 -0
  120. package/dist/runs/shared/pi-spawn.js +52 -0
  121. package/dist/runs/shared/run-history-queries.js +45 -0
  122. package/dist/runs/shared/run-history.js +110 -0
  123. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  124. package/dist/runs/shared/run-records.js +679 -0
  125. package/dist/runs/shared/session-cwd-preload.js +33 -0
  126. package/dist/runs/shared/single-output.js +222 -0
  127. package/dist/runs/shared/streaming-text.js +15 -0
  128. package/dist/runs/shared/structured-output.js +55 -0
  129. package/dist/runs/shared/subagent-control.js +179 -0
  130. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  131. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  132. package/dist/runs/shared/supervisor-questions.js +350 -0
  133. package/dist/runs/shared/workflow-graph.js +214 -0
  134. package/dist/runs/shared/workflow-policy.js +84 -0
  135. package/dist/runs/shared/worktree.js +525 -0
  136. package/dist/shared/agent-context-policy.js +126 -0
  137. package/dist/shared/artifacts.js +96 -0
  138. package/dist/shared/atomic-json.js +13 -0
  139. package/dist/shared/cooperative.js +20 -0
  140. package/dist/shared/execution-cwd.js +14 -0
  141. package/dist/shared/file-coalescer.js +21 -0
  142. package/dist/shared/fork-context.js +49 -0
  143. package/dist/shared/formatters.js +85 -0
  144. package/dist/shared/journal-reader.js +407 -0
  145. package/dist/shared/lazy-tools.js +32 -0
  146. package/dist/shared/model-info.js +68 -0
  147. package/dist/shared/native-session.js +15 -0
  148. package/dist/shared/native-tui.js +18 -0
  149. package/dist/shared/native-typebox.js +20 -0
  150. package/dist/shared/post-exit-stdio-guard.js +197 -0
  151. package/dist/shared/prompt-sections.js +8 -0
  152. package/dist/shared/session-entries.js +52 -0
  153. package/dist/shared/session-identity.js +14 -0
  154. package/dist/shared/settings.js +254 -0
  155. package/dist/shared/status-format.js +111 -0
  156. package/dist/shared/temp-root.js +114 -0
  157. package/dist/shared/types.js +207 -0
  158. package/dist/shared/ui-mode.js +3 -0
  159. package/dist/shared/utils.js +334 -0
  160. package/dist/slash/prompt-template-bridge.js +298 -0
  161. package/dist/slash/slash-bridge.js +116 -0
  162. package/dist/slash/slash-commands.js +607 -0
  163. package/dist/slash/slash-live-state.js +246 -0
  164. package/dist/tui/action-hints.js +75 -0
  165. package/dist/tui/agent-history.js +197 -0
  166. package/dist/tui/agent-view.js +1656 -0
  167. package/dist/tui/render.js +1515 -0
  168. package/docs/intercom-bridge.md +212 -0
  169. package/docs/intercom.md +581 -0
  170. package/docs/performance.md +121 -0
  171. package/package.json +102 -4
  172. package/prompts/gather-context-and-clarify.md +14 -0
  173. package/prompts/parallel-cleanup.md +62 -0
  174. package/prompts/parallel-context-build.md +56 -0
  175. package/prompts/parallel-handoff-plan.md +62 -0
  176. package/prompts/parallel-research.md +51 -0
  177. package/prompts/parallel-review.md +59 -0
  178. package/prompts/review-loop.md +42 -0
  179. package/scripts/build.mjs +134 -0
  180. package/scripts/check-compat.mjs +48 -0
  181. package/scripts/compat-host.mjs +19 -0
  182. package/scripts/intercom-bridge-credentials.mjs +143 -0
  183. package/scripts/intercom-remote.mjs +103 -0
  184. package/scripts/linux-smoke.sh +51 -0
  185. package/scripts/local-install-smoke.mjs +66 -0
  186. package/scripts/native-package-smoke.mjs +82 -0
  187. package/scripts/package-smoke.mjs +209 -0
  188. package/scripts/prepare.mjs +67 -0
  189. package/scripts/real-pi-smoke.mjs +415 -0
  190. package/scripts/run-tests.mjs +149 -0
  191. package/skills/pi-intercom/SKILL.md +113 -0
  192. package/skills/pi-intercom/evals/evals.json +29 -0
  193. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  194. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  195. package/skills/pi-subagents/SKILL.md +76 -0
  196. package/skills/pi-subagents/evals/evals.json +29 -0
  197. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  198. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  199. package/src/agents/agent-management.ts +813 -0
  200. package/src/agents/agent-scope.ts +7 -0
  201. package/src/agents/agent-selection.ts +23 -0
  202. package/src/agents/agent-serializer.ts +99 -0
  203. package/src/agents/agents.ts +916 -0
  204. package/src/agents/chain-serializer.ts +329 -0
  205. package/src/agents/frontmatter.ts +21 -0
  206. package/src/agents/identity.ts +30 -0
  207. package/src/agents/skills.ts +637 -0
  208. package/src/extension/build-info.ts +2 -0
  209. package/src/extension/compact-tools.ts +114 -0
  210. package/src/extension/config.ts +23 -0
  211. package/src/extension/control-notices.ts +47 -0
  212. package/src/extension/doctor.ts +171 -0
  213. package/src/extension/fanout-child.ts +312 -0
  214. package/src/extension/index.ts +649 -0
  215. package/src/extension/schemas.ts +444 -0
  216. package/src/extension/tool-input.ts +19 -0
  217. package/src/extension/tool-result.ts +16 -0
  218. package/src/history/canonical-result.ts +62 -0
  219. package/src/history/database.ts +59 -0
  220. package/src/history/index.ts +156 -0
  221. package/src/history/ingest.ts +349 -0
  222. package/src/history/queries.ts +241 -0
  223. package/src/history/store.ts +203 -0
  224. package/src/history/types.ts +166 -0
  225. package/src/history/worker.ts +321 -0
  226. package/src/intercom/intercom-bridge.ts +123 -0
  227. package/src/intercom/live-intercom.ts +104 -0
  228. package/src/intercom/result-intercom.ts +429 -0
  229. package/src/pi-intercom/agent-dir.ts +9 -0
  230. package/src/pi-intercom/bridge.ts +668 -0
  231. package/src/pi-intercom/broker/broker.ts +463 -0
  232. package/src/pi-intercom/broker/client.ts +558 -0
  233. package/src/pi-intercom/broker/framing.ts +77 -0
  234. package/src/pi-intercom/broker/paths.ts +47 -0
  235. package/src/pi-intercom/broker/pid.ts +59 -0
  236. package/src/pi-intercom/broker/spawn.ts +248 -0
  237. package/src/pi-intercom/config.ts +142 -0
  238. package/src/pi-intercom/index.ts +2929 -0
  239. package/src/pi-intercom/reply-tracker.ts +211 -0
  240. package/src/pi-intercom/session-targets.ts +172 -0
  241. package/src/pi-intercom/subagent-live-events.ts +133 -0
  242. package/src/pi-intercom/topics.ts +125 -0
  243. package/src/pi-intercom/types.ts +271 -0
  244. package/src/pi-intercom/ui/compose.ts +289 -0
  245. package/src/pi-intercom/ui/inline-message.ts +92 -0
  246. package/src/pi-intercom/ui/session-list.ts +139 -0
  247. package/src/runs/background/async-control.ts +89 -0
  248. package/src/runs/background/async-execution.ts +850 -0
  249. package/src/runs/background/async-job-tracker.ts +385 -0
  250. package/src/runs/background/async-result-file.ts +62 -0
  251. package/src/runs/background/async-resume.ts +333 -0
  252. package/src/runs/background/async-status.ts +481 -0
  253. package/src/runs/background/completion-dedupe.ts +79 -0
  254. package/src/runs/background/completion-delivery.ts +195 -0
  255. package/src/runs/background/notify.ts +123 -0
  256. package/src/runs/background/parallel-groups.ts +53 -0
  257. package/src/runs/background/result-watcher.ts +423 -0
  258. package/src/runs/background/run-id-resolver.ts +69 -0
  259. package/src/runs/background/run-status.ts +384 -0
  260. package/src/runs/background/stale-run-reconciler.ts +369 -0
  261. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  262. package/src/runs/background/subagent-runner.ts +2053 -0
  263. package/src/runs/background/top-level-async.ts +13 -0
  264. package/src/runs/foreground/chain-clarify.ts +1340 -0
  265. package/src/runs/foreground/clarify-invocation.ts +77 -0
  266. package/src/runs/foreground/execution-input.ts +467 -0
  267. package/src/runs/foreground/foreground-control.ts +821 -0
  268. package/src/runs/foreground/question-control.ts +82 -0
  269. package/src/runs/foreground/run-async-path.ts +243 -0
  270. package/src/runs/foreground/subagent-executor.ts +501 -0
  271. package/src/runs/foreground/subagent-params.ts +320 -0
  272. package/src/runs/foreground/wait-run.ts +124 -0
  273. package/src/runs/shared/acceptance-contract.ts +257 -0
  274. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  275. package/src/runs/shared/acceptance-finalization.ts +353 -0
  276. package/src/runs/shared/acceptance-reports.ts +152 -0
  277. package/src/runs/shared/acceptance.ts +33 -0
  278. package/src/runs/shared/chain-outputs.ts +114 -0
  279. package/src/runs/shared/child-attempt.ts +623 -0
  280. package/src/runs/shared/child-execution-cwd.ts +91 -0
  281. package/src/runs/shared/child-observations.ts +99 -0
  282. package/src/runs/shared/claude-code.ts +243 -0
  283. package/src/runs/shared/completion-guard.ts +35 -0
  284. package/src/runs/shared/dynamic-fanout.ts +295 -0
  285. package/src/runs/shared/history-index.ts +69 -0
  286. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  287. package/src/runs/shared/model-fallback.ts +219 -0
  288. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  289. package/src/runs/shared/native-finalization.ts +128 -0
  290. package/src/runs/shared/native-usage.ts +113 -0
  291. package/src/runs/shared/nested-events.ts +752 -0
  292. package/src/runs/shared/nested-path.ts +52 -0
  293. package/src/runs/shared/nested-render.ts +119 -0
  294. package/src/runs/shared/parallel-utils.ts +153 -0
  295. package/src/runs/shared/parent-receipts.ts +59 -0
  296. package/src/runs/shared/parent-usage.ts +121 -0
  297. package/src/runs/shared/pi-args.ts +350 -0
  298. package/src/runs/shared/pi-spawn.ts +52 -0
  299. package/src/runs/shared/run-history-queries.ts +44 -0
  300. package/src/runs/shared/run-history.ts +100 -0
  301. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  302. package/src/runs/shared/run-records.ts +607 -0
  303. package/src/runs/shared/session-cwd-preload.ts +27 -0
  304. package/src/runs/shared/single-output.ts +267 -0
  305. package/src/runs/shared/streaming-text.ts +18 -0
  306. package/src/runs/shared/structured-output.ts +68 -0
  307. package/src/runs/shared/subagent-control.ts +219 -0
  308. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  309. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  310. package/src/runs/shared/supervisor-questions.ts +415 -0
  311. package/src/runs/shared/workflow-graph.ts +220 -0
  312. package/src/runs/shared/workflow-policy.ts +125 -0
  313. package/src/runs/shared/worktree.ts +635 -0
  314. package/src/shared/agent-context-policy.ts +184 -0
  315. package/src/shared/artifacts.ts +95 -0
  316. package/src/shared/atomic-json.ts +16 -0
  317. package/src/shared/cooperative.ts +21 -0
  318. package/src/shared/execution-cwd.ts +14 -0
  319. package/src/shared/file-coalescer.ts +29 -0
  320. package/src/shared/fork-context.ts +68 -0
  321. package/src/shared/formatters.ts +84 -0
  322. package/src/shared/journal-reader.ts +333 -0
  323. package/src/shared/lazy-tools.ts +36 -0
  324. package/src/shared/model-info.ts +81 -0
  325. package/src/shared/native-session.ts +8 -0
  326. package/src/shared/native-tui.ts +10 -0
  327. package/src/shared/native-typebox.ts +14 -0
  328. package/src/shared/post-exit-stdio-guard.ts +204 -0
  329. package/src/shared/prompt-sections.ts +10 -0
  330. package/src/shared/session-entries.ts +49 -0
  331. package/src/shared/session-identity.ts +18 -0
  332. package/src/shared/settings.ts +384 -0
  333. package/src/shared/status-format.ts +123 -0
  334. package/src/shared/temp-root.ts +107 -0
  335. package/src/shared/types.ts +1260 -0
  336. package/src/shared/ui-mode.ts +5 -0
  337. package/src/shared/utils.ts +342 -0
  338. package/src/slash/prompt-template-bridge.ts +408 -0
  339. package/src/slash/slash-bridge.ts +178 -0
  340. package/src/slash/slash-commands.ts +617 -0
  341. package/src/slash/slash-live-state.ts +292 -0
  342. package/src/tui/action-hints.ts +69 -0
  343. package/src/tui/agent-history.ts +210 -0
  344. package/src/tui/agent-view.ts +1323 -0
  345. package/src/tui/render.ts +1550 -0
  346. package/tsconfig.build.json +12 -0
  347. package/tsconfig.json +19 -0
@@ -0,0 +1,1152 @@
1
+ /**
2
+ * Chain Clarification TUI Component
3
+ *
4
+ * Shows templates and resolved behaviors for each step in a chain.
5
+ * Supports runtime editing of templates, output paths, reads lists, and progress toggle.
6
+ */
7
+ import { matchesKey, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
8
+ import { resolveModelCandidate, splitThinkingSuffix } from "../shared/model-fallback.js";
9
+ import { findModelInfo, getSupportedThinkingLevels } from "../../shared/model-info.js";
10
+ function createEditorState(initial = "") {
11
+ return { buffer: initial, cursor: 0, viewportOffset: 0 };
12
+ }
13
+ function wrapText(text, width) {
14
+ if (width <= 0)
15
+ return { lines: [text], starts: [0] };
16
+ if (text.length === 0)
17
+ return { lines: [""], starts: [0] };
18
+ const lines = [];
19
+ const starts = [];
20
+ let offset = 0;
21
+ const segments = text.split("\n");
22
+ for (const [index, segment] of segments.entries()) {
23
+ if (segment.length === 0) {
24
+ starts.push(offset);
25
+ lines.push("");
26
+ }
27
+ else {
28
+ let lineStart = 0;
29
+ let pos = 0;
30
+ let lineWidth = 0;
31
+ while (pos < segment.length) {
32
+ const char = String.fromCodePoint(segment.codePointAt(pos));
33
+ const charWidth = visibleWidth(char);
34
+ if (lineWidth > 0 && lineWidth + charWidth > width) {
35
+ starts.push(offset + lineStart);
36
+ lines.push(segment.slice(lineStart, pos));
37
+ lineStart = pos;
38
+ lineWidth = 0;
39
+ continue;
40
+ }
41
+ pos += char.length;
42
+ lineWidth += charWidth;
43
+ }
44
+ starts.push(offset + lineStart);
45
+ lines.push(segment.slice(lineStart));
46
+ }
47
+ offset += segment.length + (index < segments.length - 1 ? 1 : 0);
48
+ }
49
+ if (!text.endsWith("\n") && text.length > 0 && visibleWidth(lines[lines.length - 1] ?? "") === width) {
50
+ starts.push(text.length);
51
+ lines.push("");
52
+ }
53
+ return { lines, starts };
54
+ }
55
+ function getCursorDisplayPos(cursor, starts) {
56
+ for (let i = starts.length - 1; i >= 0; i--) {
57
+ if (cursor >= starts[i])
58
+ return { line: i, col: cursor - starts[i] };
59
+ }
60
+ return { line: 0, col: 0 };
61
+ }
62
+ function ensureCursorVisible(cursorLine, viewportHeight, currentOffset) {
63
+ if (cursorLine < currentOffset)
64
+ return Math.max(0, cursorLine);
65
+ if (cursorLine >= currentOffset + viewportHeight)
66
+ return Math.max(0, cursorLine - viewportHeight + 1);
67
+ return Math.max(0, currentOffset);
68
+ }
69
+ function isWordChar(ch) {
70
+ const code = ch.charCodeAt(0);
71
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
72
+ }
73
+ function wordBackward(buffer, cursor) {
74
+ let pos = cursor;
75
+ while (pos > 0 && !isWordChar(buffer[pos - 1]))
76
+ pos--;
77
+ while (pos > 0 && isWordChar(buffer[pos - 1]))
78
+ pos--;
79
+ return pos;
80
+ }
81
+ function wordForward(buffer, cursor) {
82
+ let pos = cursor;
83
+ while (pos < buffer.length && isWordChar(buffer[pos]))
84
+ pos++;
85
+ while (pos < buffer.length && !isWordChar(buffer[pos]))
86
+ pos++;
87
+ return pos;
88
+ }
89
+ function normalizeInsertText(data) {
90
+ let text = data.split("\x1b[200~").join("").split("\x1b[201~").join("");
91
+ text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
92
+ const newline = text.indexOf("\n");
93
+ if (newline !== -1)
94
+ text = text.slice(0, newline);
95
+ text = text.replace(/\t/g, " ");
96
+ if (text.length === 0)
97
+ return null;
98
+ for (let i = 0; i < text.length; i++) {
99
+ if (text.charCodeAt(i) < 32)
100
+ return null;
101
+ }
102
+ return text;
103
+ }
104
+ function handleEditorInput(state, data, textWidth) {
105
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "return"))
106
+ return null;
107
+ const { lines: wrapped, starts } = wrapText(state.buffer, textWidth);
108
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
109
+ if (matchesKey(data, "alt+left") || matchesKey(data, "ctrl+left"))
110
+ return { ...state, cursor: wordBackward(state.buffer, state.cursor) };
111
+ if (matchesKey(data, "alt+right") || matchesKey(data, "ctrl+right"))
112
+ return { ...state, cursor: wordForward(state.buffer, state.cursor) };
113
+ if (matchesKey(data, "left"))
114
+ return state.cursor > 0 ? { ...state, cursor: state.cursor - 1 } : state;
115
+ if (matchesKey(data, "right"))
116
+ return state.cursor < state.buffer.length ? { ...state, cursor: state.cursor + 1 } : state;
117
+ if (matchesKey(data, "up") && cursorPos.line > 0) {
118
+ const targetLine = cursorPos.line - 1;
119
+ return { ...state, cursor: starts[targetLine] + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
120
+ }
121
+ if (matchesKey(data, "down") && cursorPos.line < wrapped.length - 1) {
122
+ const targetLine = cursorPos.line + 1;
123
+ return { ...state, cursor: starts[targetLine] + Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0) };
124
+ }
125
+ if (matchesKey(data, "home"))
126
+ return { ...state, cursor: starts[cursorPos.line] };
127
+ if (matchesKey(data, "end"))
128
+ return { ...state, cursor: starts[cursorPos.line] + (wrapped[cursorPos.line]?.length ?? 0) };
129
+ if (matchesKey(data, "ctrl+home"))
130
+ return { ...state, cursor: 0 };
131
+ if (matchesKey(data, "ctrl+end"))
132
+ return { ...state, cursor: state.buffer.length };
133
+ if (matchesKey(data, "alt+backspace")) {
134
+ const target = wordBackward(state.buffer, state.cursor);
135
+ return target === state.cursor ? state : { ...state, buffer: state.buffer.slice(0, target) + state.buffer.slice(state.cursor), cursor: target };
136
+ }
137
+ if (matchesKey(data, "backspace")) {
138
+ return state.cursor > 0
139
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor), cursor: state.cursor - 1 }
140
+ : state;
141
+ }
142
+ if (matchesKey(data, "delete")) {
143
+ return state.cursor < state.buffer.length
144
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + state.buffer.slice(state.cursor + 1) }
145
+ : state;
146
+ }
147
+ const insert = normalizeInsertText(data);
148
+ return insert
149
+ ? { ...state, buffer: state.buffer.slice(0, state.cursor) + insert + state.buffer.slice(state.cursor), cursor: state.cursor + insert.length }
150
+ : null;
151
+ }
152
+ function renderWithCursor(text, cursorPos) {
153
+ const before = text.slice(0, cursorPos);
154
+ const cursorChar = text[cursorPos] ?? " ";
155
+ const after = text.slice(cursorPos + 1);
156
+ return `${before}\x1b[7m${cursorChar}\x1b[27m${after}`;
157
+ }
158
+ function renderEditor(state, width, viewportHeight) {
159
+ const { lines: wrapped, starts } = wrapText(state.buffer, width);
160
+ const cursorPos = getCursorDisplayPos(state.cursor, starts);
161
+ const lines = [];
162
+ for (let i = 0; i < viewportHeight; i++) {
163
+ const lineIdx = state.viewportOffset + i;
164
+ let content = lineIdx < wrapped.length ? wrapped[lineIdx] ?? "" : "";
165
+ if (lineIdx === cursorPos.line)
166
+ content = renderWithCursor(content, cursorPos.col);
167
+ lines.push(content);
168
+ }
169
+ return lines;
170
+ }
171
+ /**
172
+ * TUI component for chain clarification.
173
+ * Factory signature matches ctx.ui.custom: (tui, theme, kb, done) => Component
174
+ */
175
+ export class ChainClarifyComponent {
176
+ renderWidth = 84;
177
+ get width() { return this.renderWidth; }
178
+ selectedStep = 0;
179
+ editingStep = null;
180
+ editMode = "template";
181
+ editState = createEditorState();
182
+ EDIT_VIEWPORT_HEIGHT = 12;
183
+ behaviorOverrides = new Map();
184
+ modelSearchQuery = "";
185
+ modelSelectedIndex = 0;
186
+ filteredModels = [];
187
+ MODEL_SELECTOR_HEIGHT = 10;
188
+ thinkingSelectedIndex = 0;
189
+ skillSearchQuery = "";
190
+ skillSelectedNames = new Set();
191
+ skillCursorIndex = 0;
192
+ filteredSkills = [];
193
+ noticeMessage = null;
194
+ noticeMessageTimer = null;
195
+ /** Run in background (async) mode */
196
+ runInBackground = false;
197
+ tui;
198
+ theme;
199
+ agentConfigs;
200
+ templates;
201
+ originalTask;
202
+ chainDir;
203
+ resolvedBehaviors;
204
+ availableModels;
205
+ preferredProvider;
206
+ availableSkills;
207
+ done;
208
+ mode;
209
+ constructor(tui, theme, agentConfigs, templates, originalTask, chainDir, resolvedBehaviors, availableModels, preferredProvider, availableSkills, done, mode = 'chain') {
210
+ this.tui = tui;
211
+ this.theme = theme;
212
+ this.agentConfigs = agentConfigs;
213
+ this.templates = templates;
214
+ this.originalTask = originalTask;
215
+ this.chainDir = chainDir;
216
+ this.resolvedBehaviors = resolvedBehaviors;
217
+ this.availableModels = availableModels;
218
+ this.preferredProvider = preferredProvider;
219
+ this.availableSkills = availableSkills;
220
+ this.done = done;
221
+ this.mode = mode;
222
+ this.filteredModels = [...availableModels];
223
+ this.filteredSkills = [...availableSkills];
224
+ }
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+ // Helper methods for rendering
227
+ // ─────────────────────────────────────────────────────────────────────────────
228
+ /** Pad string to specified visible width */
229
+ pad(s, len) {
230
+ const vis = visibleWidth(s);
231
+ return s + " ".repeat(Math.max(0, len - vis));
232
+ }
233
+ /** Create a row with border characters */
234
+ row(content) {
235
+ const innerW = this.width - 2;
236
+ const clipped = truncateToWidth(content, innerW, "", true);
237
+ return this.theme.fg("border", "│") + this.pad(clipped, innerW) + this.theme.fg("border", "│");
238
+ }
239
+ /** Render centered header line with border */
240
+ renderHeader(text) {
241
+ const innerW = this.width - 2;
242
+ const clipped = truncateToWidth(text, innerW, "", true);
243
+ const padLen = Math.max(0, innerW - visibleWidth(clipped));
244
+ const padLeft = Math.floor(padLen / 2);
245
+ const padRight = padLen - padLeft;
246
+ return (this.theme.fg("border", "╭" + "─".repeat(padLeft)) +
247
+ this.theme.fg("accent", clipped) +
248
+ this.theme.fg("border", "─".repeat(padRight) + "╮"));
249
+ }
250
+ /** Render centered footer line with border */
251
+ renderFooter(text) {
252
+ const innerW = this.width - 2;
253
+ const clipped = truncateToWidth(text, innerW, "", true);
254
+ const padLen = Math.max(0, innerW - visibleWidth(clipped));
255
+ const padLeft = Math.floor(padLen / 2);
256
+ const padRight = padLen - padLeft;
257
+ return (this.theme.fg("border", "╰" + "─".repeat(padLeft)) +
258
+ this.theme.fg("dim", clipped) +
259
+ this.theme.fg("border", "─".repeat(padRight) + "╯"));
260
+ }
261
+ /** Exit edit mode and reset state */
262
+ exitEditMode() {
263
+ this.editingStep = null;
264
+ this.editState = createEditorState();
265
+ this.tui.requestRender();
266
+ }
267
+ // ─────────────────────────────────────────────────────────────────────────────
268
+ // Full edit mode methods
269
+ // ─────────────────────────────────────────────────────────────────────────────
270
+ /** Render the full-edit takeover view */
271
+ renderFullEditMode() {
272
+ const innerW = this.width - 2;
273
+ const textWidth = innerW - 2; // 1 char padding on each side
274
+ const lines = [];
275
+ const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
276
+ const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
277
+ this.editState = {
278
+ ...this.editState,
279
+ viewportOffset: ensureCursorVisible(cursorPos.line, this.EDIT_VIEWPORT_HEIGHT, this.editState.viewportOffset),
280
+ };
281
+ // Header (truncate agent name to prevent overflow)
282
+ const fieldName = this.editMode === "template" ? "task" : this.editMode;
283
+ const rawAgentName = this.agentConfigs[this.editingStep]?.name ?? "unknown";
284
+ const maxAgentLen = innerW - 30; // Reserve space for " Editing X (Step/Task N: ) "
285
+ const agentName = rawAgentName.length > maxAgentLen
286
+ ? rawAgentName.slice(0, maxAgentLen - 1) + "…"
287
+ : rawAgentName;
288
+ // Use mode-appropriate terminology
289
+ const stepLabel = this.mode === 'single'
290
+ ? agentName
291
+ : this.mode === 'parallel'
292
+ ? `Task ${this.editingStep + 1}: ${agentName}`
293
+ : `Step ${this.editingStep + 1}: ${agentName}`;
294
+ const headerText = ` Editing ${fieldName} (${stepLabel}) `;
295
+ lines.push(this.renderHeader(headerText));
296
+ lines.push(this.row(""));
297
+ const editorLines = renderEditor(this.editState, textWidth, this.EDIT_VIEWPORT_HEIGHT);
298
+ for (const line of editorLines) {
299
+ lines.push(this.row(` ${line}`));
300
+ }
301
+ const linesBelow = wrapped.length - this.editState.viewportOffset - this.EDIT_VIEWPORT_HEIGHT;
302
+ const hasMore = linesBelow > 0;
303
+ const hasLess = this.editState.viewportOffset > 0;
304
+ let scrollInfo = "";
305
+ if (hasLess)
306
+ scrollInfo += "↑";
307
+ if (hasMore)
308
+ scrollInfo += `↓ ${linesBelow}+`;
309
+ lines.push(this.row(""));
310
+ const footerText = scrollInfo
311
+ ? ` [Esc] Done • [Ctrl+C] Discard • ${scrollInfo} `
312
+ : " [Esc] Done • [Ctrl+C] Discard ";
313
+ lines.push(this.renderFooter(footerText));
314
+ return lines;
315
+ }
316
+ // ─────────────────────────────────────────────────────────────────────────────
317
+ // Behavior helpers
318
+ // ─────────────────────────────────────────────────────────────────────────────
319
+ /** Get effective behavior for a step (with user overrides applied) */
320
+ getEffectiveBehavior(stepIndex) {
321
+ const base = this.resolvedBehaviors[stepIndex];
322
+ const override = this.behaviorOverrides.get(stepIndex);
323
+ if (!override)
324
+ return base;
325
+ return {
326
+ output: override.output !== undefined ? override.output : base.output,
327
+ outputMode: base.outputMode,
328
+ reads: override.reads !== undefined ? override.reads : base.reads,
329
+ progress: override.progress !== undefined ? override.progress : base.progress,
330
+ skills: override.skills !== undefined ? override.skills : base.skills,
331
+ model: override.model !== undefined ? override.model : base.model,
332
+ };
333
+ }
334
+ /** Get the effective model for a step (override or agent default) */
335
+ getEffectiveModel(stepIndex) {
336
+ const override = this.behaviorOverrides.get(stepIndex);
337
+ if (override?.model)
338
+ return this.resolveModelFullId(override.model);
339
+ const baseModel = this.resolvedBehaviors[stepIndex]?.model;
340
+ if (baseModel)
341
+ return this.resolveModelFullId(baseModel);
342
+ return "default";
343
+ }
344
+ /** Resolve a model name to its full provider/model format */
345
+ resolveModelFullId(modelName) {
346
+ return resolveModelCandidate(modelName, this.availableModels, this.preferredProvider) ?? modelName;
347
+ }
348
+ /** Update a behavior override for a step */
349
+ updateBehavior(stepIndex, field, value) {
350
+ const existing = this.behaviorOverrides.get(stepIndex) ?? {};
351
+ this.behaviorOverrides.set(stepIndex, { ...existing, [field]: value });
352
+ }
353
+ showNotice(text, type) {
354
+ this.noticeMessage = { text, type };
355
+ if (this.noticeMessageTimer)
356
+ clearTimeout(this.noticeMessageTimer);
357
+ this.noticeMessageTimer = setTimeout(() => {
358
+ this.noticeMessage = null;
359
+ this.noticeMessageTimer = null;
360
+ this.tui.requestRender();
361
+ }, 2000);
362
+ this.tui.requestRender();
363
+ }
364
+ handleInput(data) {
365
+ if (this.editingStep !== null) {
366
+ if (this.editMode === "model") {
367
+ this.handleModelSelectorInput(data);
368
+ }
369
+ else if (this.editMode === "thinking") {
370
+ this.handleThinkingSelectorInput(data);
371
+ }
372
+ else if (this.editMode === "skills") {
373
+ this.handleSkillSelectorInput(data);
374
+ }
375
+ else {
376
+ this.handleEditInput(data);
377
+ }
378
+ return;
379
+ }
380
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
381
+ this.done({ confirmed: false, templates: [], behaviorOverrides: [] });
382
+ return;
383
+ }
384
+ if (matchesKey(data, "return")) {
385
+ const overrides = [];
386
+ for (let i = 0; i < this.agentConfigs.length; i++) {
387
+ overrides.push(this.behaviorOverrides.get(i));
388
+ }
389
+ this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides, runInBackground: this.runInBackground });
390
+ return;
391
+ }
392
+ if (matchesKey(data, "up")) {
393
+ this.selectedStep = Math.max(0, this.selectedStep - 1);
394
+ this.tui.requestRender();
395
+ return;
396
+ }
397
+ if (matchesKey(data, "down")) {
398
+ const maxStep = Math.max(0, this.agentConfigs.length - 1);
399
+ this.selectedStep = Math.min(maxStep, this.selectedStep + 1);
400
+ this.tui.requestRender();
401
+ return;
402
+ }
403
+ if (data === "e") {
404
+ this.enterEditMode("template");
405
+ return;
406
+ }
407
+ if (data === "m") {
408
+ this.enterModelSelector();
409
+ return;
410
+ }
411
+ if (data === "t") {
412
+ this.enterThinkingSelector();
413
+ return;
414
+ }
415
+ if (data === "s") {
416
+ this.editingStep = this.selectedStep;
417
+ this.editMode = "skills";
418
+ this.skillSearchQuery = "";
419
+ this.skillCursorIndex = 0;
420
+ this.filteredSkills = [...this.availableSkills];
421
+ const current = this.getEffectiveBehavior(this.selectedStep).skills;
422
+ this.skillSelectedNames.clear();
423
+ if (current !== false && current.length > 0) {
424
+ current.forEach((skillName) => this.skillSelectedNames.add(skillName));
425
+ }
426
+ this.tui.requestRender();
427
+ return;
428
+ }
429
+ if (data === "w" && this.mode !== 'parallel') {
430
+ this.enterEditMode("output");
431
+ return;
432
+ }
433
+ if (data === "r" && this.mode === 'chain') {
434
+ this.enterEditMode("reads");
435
+ return;
436
+ }
437
+ if (data === "p" && this.mode === 'chain') {
438
+ const anyEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
439
+ const newState = !anyEnabled;
440
+ for (let i = 0; i < this.agentConfigs.length; i++) {
441
+ this.updateBehavior(i, "progress", newState);
442
+ }
443
+ this.tui.requestRender();
444
+ return;
445
+ }
446
+ if (data === "b") {
447
+ this.runInBackground = !this.runInBackground;
448
+ this.tui.requestRender();
449
+ return;
450
+ }
451
+ }
452
+ enterEditMode(mode) {
453
+ this.editingStep = this.selectedStep;
454
+ this.editMode = mode;
455
+ let buffer = "";
456
+ if (mode === "template") {
457
+ const template = this.templates[this.selectedStep] ?? "";
458
+ buffer = template.split("\n")[0] ?? "";
459
+ }
460
+ else if (mode === "output") {
461
+ const behavior = this.getEffectiveBehavior(this.selectedStep);
462
+ buffer = behavior.output === false ? "" : (behavior.output || "");
463
+ }
464
+ else if (mode === "reads") {
465
+ const behavior = this.getEffectiveBehavior(this.selectedStep);
466
+ buffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
467
+ }
468
+ this.editState = createEditorState(buffer);
469
+ this.tui.requestRender();
470
+ }
471
+ /** Enter model selector mode */
472
+ enterModelSelector() {
473
+ this.editingStep = this.selectedStep;
474
+ this.editMode = "model";
475
+ this.modelSearchQuery = "";
476
+ this.modelSelectedIndex = 0;
477
+ this.filteredModels = [...this.availableModels];
478
+ const currentModel = splitThinkingSuffix(this.getEffectiveModel(this.selectedStep)).baseModel;
479
+ const currentIndex = this.filteredModels.findIndex((m) => m.fullId === currentModel || m.id === currentModel);
480
+ if (currentIndex >= 0) {
481
+ this.modelSelectedIndex = currentIndex;
482
+ }
483
+ this.tui.requestRender();
484
+ }
485
+ /** Filter models based on search query */
486
+ filterModels() {
487
+ const query = this.modelSearchQuery.toLowerCase();
488
+ if (!query) {
489
+ this.filteredModels = [...this.availableModels];
490
+ }
491
+ else {
492
+ this.filteredModels = this.availableModels.filter((m) => m.fullId.toLowerCase().includes(query) ||
493
+ m.id.toLowerCase().includes(query) ||
494
+ m.provider.toLowerCase().includes(query));
495
+ }
496
+ this.modelSelectedIndex = Math.min(this.modelSelectedIndex, Math.max(0, this.filteredModels.length - 1));
497
+ }
498
+ handleModelSelectorInput(data) {
499
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
500
+ this.exitEditMode();
501
+ return;
502
+ }
503
+ if (matchesKey(data, "return")) {
504
+ const selected = this.filteredModels[this.modelSelectedIndex];
505
+ if (selected) {
506
+ const { thinkingSuffix } = splitThinkingSuffix(this.getEffectiveModel(this.editingStep));
507
+ const requestedLevel = thinkingSuffix.slice(1);
508
+ const selectedModel = findModelInfo(selected.fullId, this.availableModels, this.preferredProvider);
509
+ const suffix = getSupportedThinkingLevels(selectedModel).some((level) => level === requestedLevel) ? thinkingSuffix : "";
510
+ this.updateBehavior(this.editingStep, "model", `${selected.fullId}${suffix}`);
511
+ }
512
+ this.exitEditMode();
513
+ return;
514
+ }
515
+ if (matchesKey(data, "up")) {
516
+ if (this.filteredModels.length > 0) {
517
+ this.modelSelectedIndex = this.modelSelectedIndex === 0
518
+ ? this.filteredModels.length - 1
519
+ : this.modelSelectedIndex - 1;
520
+ }
521
+ this.tui.requestRender();
522
+ return;
523
+ }
524
+ if (matchesKey(data, "down")) {
525
+ if (this.filteredModels.length > 0) {
526
+ this.modelSelectedIndex = this.modelSelectedIndex === this.filteredModels.length - 1
527
+ ? 0
528
+ : this.modelSelectedIndex + 1;
529
+ }
530
+ this.tui.requestRender();
531
+ return;
532
+ }
533
+ if (matchesKey(data, "backspace")) {
534
+ if (this.modelSearchQuery.length > 0) {
535
+ this.modelSearchQuery = this.modelSearchQuery.slice(0, -1);
536
+ this.filterModels();
537
+ }
538
+ this.tui.requestRender();
539
+ return;
540
+ }
541
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
542
+ this.modelSearchQuery += data;
543
+ this.filterModels();
544
+ this.tui.requestRender();
545
+ return;
546
+ }
547
+ }
548
+ getAvailableThinkingLevels(stepIndex) {
549
+ return getSupportedThinkingLevels(findModelInfo(this.getEffectiveModel(stepIndex), this.availableModels, this.preferredProvider));
550
+ }
551
+ /** Enter thinking level selector mode */
552
+ enterThinkingSelector() {
553
+ if (!this.getEffectiveBehavior(this.selectedStep).model) {
554
+ this.showNotice("Select a model first", "error");
555
+ return;
556
+ }
557
+ this.editingStep = this.selectedStep;
558
+ this.editMode = "thinking";
559
+ const levels = this.getAvailableThinkingLevels(this.selectedStep);
560
+ const { thinkingSuffix } = splitThinkingSuffix(this.getEffectiveModel(this.selectedStep));
561
+ const suffix = thinkingSuffix.slice(1);
562
+ const levelIdx = levels.findIndex((level) => level === suffix);
563
+ this.thinkingSelectedIndex = levelIdx >= 0 ? levelIdx : Math.max(0, levels.indexOf("off"));
564
+ this.tui.requestRender();
565
+ }
566
+ handleThinkingSelectorInput(data) {
567
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
568
+ this.exitEditMode();
569
+ return;
570
+ }
571
+ const levels = this.getAvailableThinkingLevels(this.editingStep);
572
+ if (levels.length === 0)
573
+ return;
574
+ if (matchesKey(data, "return")) {
575
+ const selectedLevel = levels[this.thinkingSelectedIndex] ?? "off";
576
+ this.applyThinkingLevel(selectedLevel);
577
+ this.exitEditMode();
578
+ return;
579
+ }
580
+ if (matchesKey(data, "up")) {
581
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === 0
582
+ ? levels.length - 1
583
+ : this.thinkingSelectedIndex - 1;
584
+ this.tui.requestRender();
585
+ return;
586
+ }
587
+ if (matchesKey(data, "down")) {
588
+ this.thinkingSelectedIndex = this.thinkingSelectedIndex === levels.length - 1
589
+ ? 0
590
+ : this.thinkingSelectedIndex + 1;
591
+ this.tui.requestRender();
592
+ return;
593
+ }
594
+ }
595
+ /** Apply thinking level to the current step's model */
596
+ applyThinkingLevel(level) {
597
+ const stepIndex = this.editingStep;
598
+ const currentModel = this.getEffectiveBehavior(stepIndex).model;
599
+ if (!currentModel)
600
+ return;
601
+ const { baseModel } = splitThinkingSuffix(currentModel);
602
+ const newModel = level === "off" ? baseModel : `${baseModel}:${level}`;
603
+ this.updateBehavior(stepIndex, "model", newModel);
604
+ }
605
+ filterSkills() {
606
+ const query = this.skillSearchQuery.toLowerCase();
607
+ if (!query) {
608
+ this.filteredSkills = [...this.availableSkills];
609
+ }
610
+ else {
611
+ this.filteredSkills = this.availableSkills.filter((s) => s.name.toLowerCase().includes(query) ||
612
+ (s.description?.toLowerCase().includes(query) ?? false));
613
+ }
614
+ this.skillCursorIndex = Math.min(this.skillCursorIndex, Math.max(0, this.filteredSkills.length - 1));
615
+ }
616
+ handleSkillSelectorInput(data) {
617
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
618
+ this.exitEditMode();
619
+ return;
620
+ }
621
+ if (matchesKey(data, "return")) {
622
+ const selected = [...this.skillSelectedNames];
623
+ this.updateBehavior(this.editingStep, "skills", selected);
624
+ this.exitEditMode();
625
+ return;
626
+ }
627
+ if (data === " ") {
628
+ if (this.filteredSkills.length > 0) {
629
+ const skill = this.filteredSkills[this.skillCursorIndex];
630
+ if (skill) {
631
+ if (this.skillSelectedNames.has(skill.name)) {
632
+ this.skillSelectedNames.delete(skill.name);
633
+ }
634
+ else {
635
+ this.skillSelectedNames.add(skill.name);
636
+ }
637
+ }
638
+ }
639
+ this.tui.requestRender();
640
+ return;
641
+ }
642
+ if (matchesKey(data, "up")) {
643
+ if (this.filteredSkills.length > 0) {
644
+ this.skillCursorIndex = this.skillCursorIndex === 0
645
+ ? this.filteredSkills.length - 1
646
+ : this.skillCursorIndex - 1;
647
+ }
648
+ this.tui.requestRender();
649
+ return;
650
+ }
651
+ if (matchesKey(data, "down")) {
652
+ if (this.filteredSkills.length > 0) {
653
+ this.skillCursorIndex = this.skillCursorIndex === this.filteredSkills.length - 1
654
+ ? 0
655
+ : this.skillCursorIndex + 1;
656
+ }
657
+ this.tui.requestRender();
658
+ return;
659
+ }
660
+ if (matchesKey(data, "backspace")) {
661
+ if (this.skillSearchQuery.length > 0) {
662
+ this.skillSearchQuery = this.skillSearchQuery.slice(0, -1);
663
+ this.filterSkills();
664
+ }
665
+ this.tui.requestRender();
666
+ return;
667
+ }
668
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
669
+ this.skillSearchQuery += data;
670
+ this.filterSkills();
671
+ this.tui.requestRender();
672
+ return;
673
+ }
674
+ }
675
+ handleEditInput(data) {
676
+ const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
677
+ if (matchesKey(data, "shift+up") || matchesKey(data, "pageUp")) {
678
+ const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
679
+ const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
680
+ const targetLine = Math.max(0, cursorPos.line - this.EDIT_VIEWPORT_HEIGHT);
681
+ const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
682
+ this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
683
+ this.tui.requestRender();
684
+ return;
685
+ }
686
+ if (matchesKey(data, "shift+down") || matchesKey(data, "pageDown")) {
687
+ const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
688
+ const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
689
+ const targetLine = Math.min(wrapped.length - 1, cursorPos.line + this.EDIT_VIEWPORT_HEIGHT);
690
+ const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
691
+ this.editState = { ...this.editState, cursor: starts[targetLine] + targetCol };
692
+ this.tui.requestRender();
693
+ return;
694
+ }
695
+ if (matchesKey(data, "tab"))
696
+ return;
697
+ const nextState = handleEditorInput(this.editState, data, textWidth);
698
+ if (nextState) {
699
+ this.editState = nextState;
700
+ this.tui.requestRender();
701
+ return;
702
+ }
703
+ if (matchesKey(data, "escape")) {
704
+ this.saveEdit();
705
+ this.exitEditMode();
706
+ return;
707
+ }
708
+ if (matchesKey(data, "ctrl+c")) {
709
+ this.exitEditMode();
710
+ return;
711
+ }
712
+ }
713
+ saveEdit() {
714
+ const stepIndex = this.editingStep;
715
+ if (this.editMode === "template") {
716
+ // For template, preserve other lines if they existed
717
+ const original = this.templates[stepIndex] ?? "";
718
+ const originalLines = original.split("\n");
719
+ originalLines[0] = this.editState.buffer;
720
+ this.templates[stepIndex] = originalLines.join("\n");
721
+ }
722
+ else if (this.editMode === "output") {
723
+ // Capture OLD output before updating (for downstream propagation)
724
+ const oldBehavior = this.getEffectiveBehavior(stepIndex);
725
+ const oldOutput = typeof oldBehavior.output === "string" ? oldBehavior.output : null;
726
+ // Empty string or whitespace means disable output
727
+ const trimmed = this.editState.buffer.trim();
728
+ const newOutput = trimmed === "" ? false : trimmed;
729
+ this.updateBehavior(stepIndex, "output", newOutput);
730
+ // Propagate output filename change to downstream steps' reads
731
+ if (oldOutput && typeof newOutput === "string" && oldOutput !== newOutput) {
732
+ this.propagateOutputChange(stepIndex, oldOutput, newOutput);
733
+ }
734
+ }
735
+ else if (this.editMode === "reads") {
736
+ // Parse comma-separated list, empty means disable reads
737
+ const trimmed = this.editState.buffer.trim();
738
+ if (trimmed === "") {
739
+ this.updateBehavior(stepIndex, "reads", false);
740
+ }
741
+ else {
742
+ const files = trimmed.split(",").map(f => f.trim()).filter(f => f !== "");
743
+ this.updateBehavior(stepIndex, "reads", files.length > 0 ? files : false);
744
+ }
745
+ }
746
+ }
747
+ /**
748
+ * When a step's output filename changes, update downstream steps that read from it.
749
+ * This maintains the chain dependency automatically.
750
+ */
751
+ propagateOutputChange(changedStepIndex, oldOutput, newOutput) {
752
+ // Check all downstream steps (steps that come after the changed step)
753
+ for (let i = changedStepIndex + 1; i < this.agentConfigs.length; i++) {
754
+ const behavior = this.getEffectiveBehavior(i);
755
+ // Skip if reads is disabled or empty
756
+ if (behavior.reads === false || !behavior.reads || behavior.reads.length === 0) {
757
+ continue;
758
+ }
759
+ // Check if this step reads the old output file
760
+ const readsArray = behavior.reads;
761
+ const oldIndex = readsArray.indexOf(oldOutput);
762
+ if (oldIndex !== -1) {
763
+ // Replace old filename with new filename in reads
764
+ const newReads = [...readsArray];
765
+ newReads[oldIndex] = newOutput;
766
+ this.updateBehavior(i, "reads", newReads);
767
+ }
768
+ }
769
+ }
770
+ render(width) {
771
+ if (width < 40)
772
+ return [truncateToWidth("Subagent clarify: widen terminal", width)];
773
+ this.renderWidth = Math.min(84, width);
774
+ if (this.editingStep !== null) {
775
+ if (this.editMode === "model") {
776
+ return this.renderModelSelector();
777
+ }
778
+ if (this.editMode === "thinking") {
779
+ return this.renderThinkingSelector();
780
+ }
781
+ if (this.editMode === "skills") {
782
+ return this.renderSkillSelector();
783
+ }
784
+ return this.renderFullEditMode();
785
+ }
786
+ // Mode-based navigation rendering
787
+ switch (this.mode) {
788
+ case 'single': return this.renderSingleMode();
789
+ case 'parallel': return this.renderParallelMode();
790
+ case 'chain': return this.renderChainMode();
791
+ }
792
+ }
793
+ /** Render the model selector view */
794
+ renderModelSelector() {
795
+ const th = this.theme;
796
+ const lines = [];
797
+ // Header (mode-aware terminology)
798
+ const agentName = this.agentConfigs[this.editingStep]?.name ?? "unknown";
799
+ const stepLabel = this.mode === 'single'
800
+ ? agentName
801
+ : this.mode === 'parallel'
802
+ ? `Task ${this.editingStep + 1}: ${agentName}`
803
+ : `Step ${this.editingStep + 1}: ${agentName}`;
804
+ const headerText = ` Select Model (${stepLabel}) `;
805
+ lines.push(this.renderHeader(headerText));
806
+ lines.push(this.row(""));
807
+ const searchPrefix = th.fg("dim", "Search: ");
808
+ const cursor = "\x1b[7m \x1b[27m"; // Reverse video space for cursor
809
+ const searchDisplay = this.modelSearchQuery + cursor;
810
+ lines.push(this.row(` ${searchPrefix}${searchDisplay}`));
811
+ lines.push(this.row(""));
812
+ const currentModel = this.getEffectiveModel(this.editingStep);
813
+ const currentModelBase = splitThinkingSuffix(currentModel).baseModel;
814
+ const currentLabel = th.fg("dim", "Current: ");
815
+ lines.push(this.row(` ${currentLabel}${th.fg("warning", currentModel)}`));
816
+ lines.push(this.row(""));
817
+ if (this.filteredModels.length === 0) {
818
+ lines.push(this.row(` ${th.fg("dim", "No matching models")}`));
819
+ }
820
+ else {
821
+ const maxVisible = this.MODEL_SELECTOR_HEIGHT;
822
+ let startIdx = 0;
823
+ if (this.filteredModels.length > maxVisible) {
824
+ startIdx = Math.max(0, this.modelSelectedIndex - Math.floor(maxVisible / 2));
825
+ startIdx = Math.min(startIdx, this.filteredModels.length - maxVisible);
826
+ }
827
+ const endIdx = Math.min(startIdx + maxVisible, this.filteredModels.length);
828
+ if (startIdx > 0) {
829
+ lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
830
+ }
831
+ for (let i = startIdx; i < endIdx; i++) {
832
+ const model = this.filteredModels[i];
833
+ const isSelected = i === this.modelSelectedIndex;
834
+ const isCurrent = model.fullId === currentModelBase || model.id === currentModelBase;
835
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
836
+ const modelText = isSelected ? th.fg("accent", model.id) : model.id;
837
+ const providerBadge = th.fg("dim", ` [${model.provider}]`);
838
+ const currentBadge = isCurrent ? th.fg("success", " current") : "";
839
+ lines.push(this.row(` ${prefix}${modelText}${providerBadge}${currentBadge}`));
840
+ }
841
+ const remaining = this.filteredModels.length - endIdx;
842
+ if (remaining > 0) {
843
+ lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
844
+ }
845
+ }
846
+ const contentLines = lines.length;
847
+ const targetHeight = 18;
848
+ for (let i = contentLines; i < targetHeight; i++) {
849
+ lines.push(this.row(""));
850
+ }
851
+ const footerText = " [Enter] Select • [Esc] Cancel • Type to search ";
852
+ lines.push(this.renderFooter(footerText));
853
+ return lines;
854
+ }
855
+ /** Render the thinking level selector view */
856
+ renderThinkingSelector() {
857
+ const th = this.theme;
858
+ const lines = [];
859
+ const agentName = this.agentConfigs[this.editingStep]?.name ?? "unknown";
860
+ const stepLabel = this.mode === 'single'
861
+ ? agentName
862
+ : this.mode === 'parallel'
863
+ ? `Task ${this.editingStep + 1}: ${agentName}`
864
+ : `Step ${this.editingStep + 1}: ${agentName}`;
865
+ const headerText = ` Thinking Level (${stepLabel}) `;
866
+ lines.push(this.renderHeader(headerText));
867
+ lines.push(this.row(""));
868
+ const currentModel = this.getEffectiveModel(this.editingStep);
869
+ const currentLabel = th.fg("dim", "Model: ");
870
+ lines.push(this.row(` ${currentLabel}${th.fg("accent", currentModel)}`));
871
+ lines.push(this.row(""));
872
+ lines.push(this.row(` ${th.fg("dim", "Select thinking level (extended thinking budget):")}`));
873
+ lines.push(this.row(""));
874
+ const levelDescriptions = {
875
+ "off": "No extended thinking",
876
+ "minimal": "Brief reasoning",
877
+ "low": "Light reasoning",
878
+ "medium": "Moderate reasoning",
879
+ "high": "Deep reasoning",
880
+ "xhigh": "Extra-high reasoning (ultrathink)",
881
+ "max": "Maximum reasoning",
882
+ };
883
+ const levels = this.getAvailableThinkingLevels(this.editingStep);
884
+ if (levels.length === 0) {
885
+ lines.push(this.row(` ${th.fg("dim", "No supported thinking levels")}`));
886
+ }
887
+ else {
888
+ for (let i = 0; i < levels.length; i++) {
889
+ const level = levels[i];
890
+ const isSelected = i === this.thinkingSelectedIndex;
891
+ const prefix = isSelected ? th.fg("accent", "→ ") : " ";
892
+ const levelText = isSelected ? th.fg("accent", level) : level;
893
+ const desc = th.fg("dim", ` - ${levelDescriptions[level]}`);
894
+ lines.push(this.row(` ${prefix}${levelText}${desc}`));
895
+ }
896
+ }
897
+ const contentLines = lines.length;
898
+ const targetHeight = 16;
899
+ for (let i = contentLines; i < targetHeight; i++) {
900
+ lines.push(this.row(""));
901
+ }
902
+ const footerText = levels.length === 0
903
+ ? " [Esc] Cancel "
904
+ : " [Enter] Select • [Esc] Cancel • ↑↓ Navigate ";
905
+ lines.push(this.renderFooter(footerText));
906
+ return lines;
907
+ }
908
+ renderSkillSelector() {
909
+ const innerW = this.width - 2;
910
+ const th = this.theme;
911
+ const lines = [];
912
+ const agentName = this.agentConfigs[this.editingStep]?.name ?? "unknown";
913
+ const stepLabel = this.mode === 'single'
914
+ ? agentName
915
+ : this.mode === 'parallel'
916
+ ? `Task ${this.editingStep + 1}: ${agentName}`
917
+ : `Step ${this.editingStep + 1}: ${agentName}`;
918
+ lines.push(this.renderHeader(` Select Skills (${stepLabel}) `));
919
+ lines.push(this.row(""));
920
+ const cursor = "\x1b[7m \x1b[27m";
921
+ lines.push(this.row(` ${th.fg("dim", "Search: ")}${this.skillSearchQuery}${cursor}`));
922
+ lines.push(this.row(""));
923
+ const selected = [...this.skillSelectedNames].join(", ") || th.fg("dim", "(none)");
924
+ lines.push(this.row(` ${th.fg("dim", "Selected: ")}${truncateToWidth(selected, innerW - 12)}`));
925
+ lines.push(this.row(""));
926
+ const selectorHeight = 10;
927
+ if (this.filteredSkills.length === 0) {
928
+ lines.push(this.row(` ${th.fg("dim", "No matching skills")}`));
929
+ }
930
+ else {
931
+ let startIdx = 0;
932
+ if (this.filteredSkills.length > selectorHeight) {
933
+ startIdx = Math.max(0, this.skillCursorIndex - Math.floor(selectorHeight / 2));
934
+ startIdx = Math.min(startIdx, this.filteredSkills.length - selectorHeight);
935
+ }
936
+ const endIdx = Math.min(startIdx + selectorHeight, this.filteredSkills.length);
937
+ if (startIdx > 0) {
938
+ lines.push(this.row(` ${th.fg("dim", ` ↑ ${startIdx} more`)}`));
939
+ }
940
+ for (let i = startIdx; i < endIdx; i++) {
941
+ const skill = this.filteredSkills[i];
942
+ const isCursor = i === this.skillCursorIndex;
943
+ const isSelected = this.skillSelectedNames.has(skill.name);
944
+ const prefix = isCursor ? th.fg("accent", "→ ") : " ";
945
+ const checkbox = isSelected ? th.fg("success", "[x]") : "[ ]";
946
+ const nameText = isCursor ? th.fg("accent", skill.name) : skill.name;
947
+ const sourceBadge = th.fg("dim", ` [${skill.source}]`);
948
+ const desc = skill.description
949
+ ? th.fg("dim", ` - ${truncateToWidth(skill.description, 25)}`)
950
+ : "";
951
+ lines.push(this.row(` ${prefix}${checkbox} ${nameText}${sourceBadge}${desc}`));
952
+ }
953
+ const remaining = this.filteredSkills.length - endIdx;
954
+ if (remaining > 0) {
955
+ lines.push(this.row(` ${th.fg("dim", ` ↓ ${remaining} more`)}`));
956
+ }
957
+ }
958
+ const targetHeight = 18;
959
+ for (let i = lines.length; i < targetHeight; i++) {
960
+ lines.push(this.row(""));
961
+ }
962
+ lines.push(this.renderFooter(" [Enter] Confirm • [Space] Toggle • [Esc] Cancel "));
963
+ return lines;
964
+ }
965
+ getFooterText() {
966
+ const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
967
+ switch (this.mode) {
968
+ case 'single':
969
+ return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} `;
970
+ case 'parallel':
971
+ return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} • ↑↓ Nav `;
972
+ case 'chain':
973
+ return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} • ↑↓ Nav `;
974
+ }
975
+ }
976
+ appendNotice(lines) {
977
+ if (!this.noticeMessage)
978
+ return;
979
+ const color = this.noticeMessage.type === "error" ? "error" : "success";
980
+ lines.push(this.row(` ${this.theme.fg(color, this.noticeMessage.text)}`));
981
+ }
982
+ renderSingleMode() {
983
+ const innerW = this.width - 2;
984
+ const th = this.theme;
985
+ const lines = [];
986
+ const agentName = this.agentConfigs[0]?.name ?? "unknown";
987
+ const maxHeaderLen = innerW - 4;
988
+ const headerText = ` Agent: ${truncateToWidth(agentName, maxHeaderLen - 9)} `;
989
+ lines.push(this.renderHeader(headerText));
990
+ lines.push(this.row(""));
991
+ const config = this.agentConfigs[0];
992
+ const behavior = this.getEffectiveBehavior(0);
993
+ const stepLabel = config.name;
994
+ lines.push(this.row(` ${th.fg("accent", "▶ " + stepLabel)}`));
995
+ const template = (this.templates[0] ?? "").split("\n")[0] ?? "";
996
+ const taskLabel = th.fg("dim", "task: ");
997
+ lines.push(this.row(` ${taskLabel}${truncateToWidth(template, innerW - 12)}`));
998
+ const effectiveModel = this.getEffectiveModel(0);
999
+ const override = this.behaviorOverrides.get(0);
1000
+ const isOverridden = override?.model !== undefined;
1001
+ const modelValue = isOverridden
1002
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1003
+ : effectiveModel;
1004
+ const modelLabel = th.fg("dim", "model: ");
1005
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1006
+ const writesValue = behavior.output === false
1007
+ ? th.fg("dim", "(disabled)")
1008
+ : (behavior.output || th.fg("dim", "(none)"));
1009
+ const writesLabel = th.fg("dim", "writes: ");
1010
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1011
+ const skillsValue = behavior.skills === false
1012
+ ? th.fg("dim", "(disabled)")
1013
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1014
+ const skillsLabel = th.fg("dim", "skills: ");
1015
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1016
+ lines.push(this.row(""));
1017
+ this.appendNotice(lines);
1018
+ lines.push(this.renderFooter(this.getFooterText()));
1019
+ return lines;
1020
+ }
1021
+ renderParallelMode() {
1022
+ const innerW = this.width - 2;
1023
+ const th = this.theme;
1024
+ const lines = [];
1025
+ const headerText = ` Parallel Tasks (${this.agentConfigs.length}) `;
1026
+ lines.push(this.renderHeader(headerText));
1027
+ lines.push(this.row(""));
1028
+ for (let i = 0; i < this.agentConfigs.length; i++) {
1029
+ const config = this.agentConfigs[i];
1030
+ const isSelected = i === this.selectedStep;
1031
+ const color = isSelected ? "accent" : "dim";
1032
+ const prefix = isSelected ? "▶ " : " ";
1033
+ const taskPrefix = `Task ${i + 1}: `;
1034
+ const maxNameLen = innerW - 4 - prefix.length - taskPrefix.length;
1035
+ const agentName = config.name.length > maxNameLen
1036
+ ? config.name.slice(0, maxNameLen - 1) + "…"
1037
+ : config.name;
1038
+ const taskLabel = `${taskPrefix}${agentName}`;
1039
+ lines.push(this.row(` ${th.fg(color, prefix + taskLabel)}`));
1040
+ const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
1041
+ const taskTextLabel = th.fg("dim", "task: ");
1042
+ lines.push(this.row(` ${taskTextLabel}${truncateToWidth(template, innerW - 12)}`));
1043
+ const effectiveModel = this.getEffectiveModel(i);
1044
+ const override = this.behaviorOverrides.get(i);
1045
+ const isOverridden = override?.model !== undefined;
1046
+ const modelValue = isOverridden
1047
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1048
+ : effectiveModel;
1049
+ const modelLabel = th.fg("dim", "model: ");
1050
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1051
+ const behavior = this.getEffectiveBehavior(i);
1052
+ const skillsValue = behavior.skills === false
1053
+ ? th.fg("dim", "(disabled)")
1054
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1055
+ const skillsLabel = th.fg("dim", "skills: ");
1056
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1057
+ lines.push(this.row(""));
1058
+ }
1059
+ this.appendNotice(lines);
1060
+ lines.push(this.renderFooter(this.getFooterText()));
1061
+ return lines;
1062
+ }
1063
+ renderChainMode() {
1064
+ const innerW = this.width - 2;
1065
+ const th = this.theme;
1066
+ const lines = [];
1067
+ const chainLabel = this.agentConfigs.map((c) => c.name).join(" → ");
1068
+ const maxHeaderLen = innerW - 4;
1069
+ const headerText = ` Chain: ${truncateToWidth(chainLabel, maxHeaderLen - 9)} `;
1070
+ lines.push(this.renderHeader(headerText));
1071
+ lines.push(this.row(""));
1072
+ const taskPreview = truncateToWidth(this.originalTask, innerW - 16);
1073
+ lines.push(this.row(` Original Task: ${taskPreview}`));
1074
+ const chainDirPreview = truncateToWidth(this.chainDir ?? "", innerW - 12);
1075
+ lines.push(this.row(` Chain Dir: ${th.fg("dim", chainDirPreview)}`));
1076
+ const progressEnabled = this.agentConfigs.some((_, i) => this.getEffectiveBehavior(i).progress);
1077
+ const progressValue = progressEnabled ? th.fg("success", "enabled") : th.fg("dim", "disabled");
1078
+ lines.push(this.row(` Progress: ${progressValue} ${th.fg("dim", "(press [p] to toggle)")}`));
1079
+ lines.push(this.row(""));
1080
+ for (let i = 0; i < this.agentConfigs.length; i++) {
1081
+ const config = this.agentConfigs[i];
1082
+ const isSelected = i === this.selectedStep;
1083
+ const behavior = this.getEffectiveBehavior(i);
1084
+ const color = isSelected ? "accent" : "dim";
1085
+ const prefix = isSelected ? "▶ " : " ";
1086
+ const stepPrefix = `Step ${i + 1}: `;
1087
+ const maxNameLen = innerW - 4 - prefix.length - stepPrefix.length;
1088
+ const agentName = config.name.length > maxNameLen
1089
+ ? config.name.slice(0, maxNameLen - 1) + "…"
1090
+ : config.name;
1091
+ const stepLabel = `${stepPrefix}${agentName}`;
1092
+ lines.push(this.row(` ${th.fg(color, prefix + stepLabel)}`));
1093
+ const template = (this.templates[i] ?? "").split("\n")[0] ?? "";
1094
+ const highlighted = template
1095
+ .replace(/\{task\}/g, th.fg("success", "{task}"))
1096
+ .replace(/\{previous\}/g, th.fg("warning", "{previous}"))
1097
+ .replace(/\{chain_dir\}/g, th.fg("accent", "{chain_dir}"));
1098
+ const templateLabel = th.fg("dim", "task: ");
1099
+ lines.push(this.row(` ${templateLabel}${truncateToWidth(highlighted, innerW - 12)}`));
1100
+ const effectiveModel = this.getEffectiveModel(i);
1101
+ const override = this.behaviorOverrides.get(i);
1102
+ const isOverridden = override?.model !== undefined;
1103
+ const modelValue = isOverridden
1104
+ ? th.fg("warning", effectiveModel) + th.fg("dim", " ✎")
1105
+ : effectiveModel;
1106
+ const modelLabel = th.fg("dim", "model: ");
1107
+ lines.push(this.row(` ${modelLabel}${truncateToWidth(modelValue, innerW - 13)}`));
1108
+ const writesValue = behavior.output === false
1109
+ ? th.fg("dim", "(disabled)")
1110
+ : (behavior.output || th.fg("dim", "(none)"));
1111
+ const writesLabel = th.fg("dim", "writes: ");
1112
+ lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
1113
+ const readsValue = behavior.reads === false
1114
+ ? th.fg("dim", "(disabled)")
1115
+ : (behavior.reads && behavior.reads.length > 0
1116
+ ? behavior.reads.join(", ")
1117
+ : th.fg("dim", "(none)"));
1118
+ const readsLabel = th.fg("dim", "reads: ");
1119
+ lines.push(this.row(` ${readsLabel}${truncateToWidth(readsValue, innerW - 13)}`));
1120
+ const skillsValue = behavior.skills === false
1121
+ ? th.fg("dim", "(disabled)")
1122
+ : (behavior.skills?.length ? behavior.skills.join(", ") : th.fg("dim", "(none)"));
1123
+ const skillsLabel = th.fg("dim", "skills: ");
1124
+ lines.push(this.row(` ${skillsLabel}${truncateToWidth(skillsValue, innerW - 14)}`));
1125
+ if (progressEnabled) {
1126
+ const isFirstStep = i === 0;
1127
+ const progressAction = isFirstStep
1128
+ ? th.fg("success", "writes progress.md")
1129
+ : th.fg("accent", "reads progress.md");
1130
+ const progressLabel = th.fg("dim", "progress: ");
1131
+ lines.push(this.row(` ${progressLabel}${progressAction}`));
1132
+ }
1133
+ if (i < this.agentConfigs.length - 1) {
1134
+ const nextStepUsePrevious = (this.templates[i + 1] ?? "").includes("{previous}");
1135
+ if (nextStepUsePrevious) {
1136
+ const indicator = th.fg("dim", " ↳ response → ") + th.fg("warning", "{previous}");
1137
+ lines.push(this.row(indicator));
1138
+ }
1139
+ }
1140
+ lines.push(this.row(""));
1141
+ }
1142
+ this.appendNotice(lines);
1143
+ lines.push(this.renderFooter(this.getFooterText()));
1144
+ return lines;
1145
+ }
1146
+ invalidate() { }
1147
+ dispose() {
1148
+ if (this.noticeMessageTimer)
1149
+ clearTimeout(this.noticeMessageTimer);
1150
+ this.noticeMessageTimer = null;
1151
+ }
1152
+ }