@duckmind/dm-darwin-x64 0.43.2 → 0.43.8

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