@chengchenccc/oh-my-agent 0.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +79 -0
  2. package/dist/cli/args.d.ts +24 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +120 -0
  5. package/dist/cli/initial-input.d.ts +32 -0
  6. package/dist/cli/initial-input.d.ts.map +1 -0
  7. package/dist/cli/initial-input.js +131 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +3 -0
  11. package/dist/core/__fixtures__/echo-model.d.ts +6 -0
  12. package/dist/core/__fixtures__/echo-model.d.ts.map +1 -0
  13. package/dist/core/__fixtures__/echo-model.js +10 -0
  14. package/dist/core/__fixtures__/mcp-echo-server.d.ts +2 -0
  15. package/dist/core/__fixtures__/mcp-echo-server.d.ts.map +1 -0
  16. package/dist/core/__fixtures__/mcp-echo-server.js +54 -0
  17. package/dist/core/agent-runtime.d.ts +21 -0
  18. package/dist/core/agent-runtime.d.ts.map +1 -0
  19. package/dist/core/agent-runtime.js +14 -0
  20. package/dist/core/memory/autonomous-memory.d.ts +21 -0
  21. package/dist/core/memory/autonomous-memory.d.ts.map +1 -0
  22. package/dist/core/memory/autonomous-memory.js +202 -0
  23. package/dist/core/persistence/in-memory-session-store.d.ts +3 -0
  24. package/dist/core/persistence/in-memory-session-store.d.ts.map +1 -0
  25. package/dist/core/persistence/in-memory-session-store.js +109 -0
  26. package/dist/core/persistence/session-store.contract.d.ts +5 -0
  27. package/dist/core/persistence/session-store.contract.d.ts.map +1 -0
  28. package/dist/core/persistence/session-store.contract.js +183 -0
  29. package/dist/core/persistence/session-store.d.ts +33 -0
  30. package/dist/core/persistence/session-store.d.ts.map +1 -0
  31. package/dist/core/persistence/session-store.js +22 -0
  32. package/dist/core/persistence/session-tree.d.ts +49 -0
  33. package/dist/core/persistence/session-tree.d.ts.map +1 -0
  34. package/dist/core/persistence/session-tree.js +1 -0
  35. package/dist/core/plugins/plugin-code.d.ts +13 -0
  36. package/dist/core/plugins/plugin-code.d.ts.map +1 -0
  37. package/dist/core/plugins/plugin-code.js +62 -0
  38. package/dist/core/plugins/plugin-marketplace.d.ts +72 -0
  39. package/dist/core/plugins/plugin-marketplace.d.ts.map +1 -0
  40. package/dist/core/plugins/plugin-marketplace.js +340 -0
  41. package/dist/core/plugins/plugin-resolve.d.ts +32 -0
  42. package/dist/core/plugins/plugin-resolve.d.ts.map +1 -0
  43. package/dist/core/plugins/plugin-resolve.js +101 -0
  44. package/dist/core/plugins/plugin-trust.d.ts +24 -0
  45. package/dist/core/plugins/plugin-trust.d.ts.map +1 -0
  46. package/dist/core/plugins/plugin-trust.js +89 -0
  47. package/dist/core/runtime/agent-event.d.ts +98 -0
  48. package/dist/core/runtime/agent-event.d.ts.map +1 -0
  49. package/dist/core/runtime/agent-event.js +1 -0
  50. package/dist/core/runtime/agent-loop.d.ts +142 -0
  51. package/dist/core/runtime/agent-loop.d.ts.map +1 -0
  52. package/dist/core/runtime/agent-loop.js +1008 -0
  53. package/dist/core/runtime/approval.d.ts +23 -0
  54. package/dist/core/runtime/approval.d.ts.map +1 -0
  55. package/dist/core/runtime/approval.js +25 -0
  56. package/dist/core/runtime/compaction.d.ts +22 -0
  57. package/dist/core/runtime/compaction.d.ts.map +1 -0
  58. package/dist/core/runtime/compaction.js +90 -0
  59. package/dist/core/runtime/context-estimate.d.ts +42 -0
  60. package/dist/core/runtime/context-estimate.d.ts.map +1 -0
  61. package/dist/core/runtime/context-estimate.js +34 -0
  62. package/dist/core/runtime/create-runtime.d.ts +81 -0
  63. package/dist/core/runtime/create-runtime.d.ts.map +1 -0
  64. package/dist/core/runtime/create-runtime.js +250 -0
  65. package/dist/core/runtime/fake-provider.d.ts +3 -0
  66. package/dist/core/runtime/fake-provider.d.ts.map +1 -0
  67. package/dist/core/runtime/fake-provider.js +87 -0
  68. package/dist/core/runtime/logger.d.ts +13 -0
  69. package/dist/core/runtime/logger.d.ts.map +1 -0
  70. package/dist/core/runtime/logger.js +38 -0
  71. package/dist/core/runtime/loop-input.d.ts +39 -0
  72. package/dist/core/runtime/loop-input.d.ts.map +1 -0
  73. package/dist/core/runtime/loop-input.js +38 -0
  74. package/dist/core/runtime/message-cache.d.ts +16 -0
  75. package/dist/core/runtime/message-cache.d.ts.map +1 -0
  76. package/dist/core/runtime/message-cache.js +23 -0
  77. package/dist/core/runtime/model-catalog.d.ts +10 -0
  78. package/dist/core/runtime/model-catalog.d.ts.map +1 -0
  79. package/dist/core/runtime/model-catalog.js +14 -0
  80. package/dist/core/runtime/plugin-runtime.d.ts +41 -0
  81. package/dist/core/runtime/plugin-runtime.d.ts.map +1 -0
  82. package/dist/core/runtime/plugin-runtime.js +1 -0
  83. package/dist/core/runtime/plugin.d.ts +86 -0
  84. package/dist/core/runtime/plugin.d.ts.map +1 -0
  85. package/dist/core/runtime/plugin.js +23 -0
  86. package/dist/core/runtime/process-tree.d.ts +12 -0
  87. package/dist/core/runtime/process-tree.d.ts.map +1 -0
  88. package/dist/core/runtime/process-tree.js +40 -0
  89. package/dist/core/runtime/prompt.d.ts +23 -0
  90. package/dist/core/runtime/prompt.d.ts.map +1 -0
  91. package/dist/core/runtime/prompt.js +29 -0
  92. package/dist/core/runtime/prompts.d.ts +16 -0
  93. package/dist/core/runtime/prompts.d.ts.map +1 -0
  94. package/dist/core/runtime/prompts.js +36 -0
  95. package/dist/core/runtime/retry.d.ts +20 -0
  96. package/dist/core/runtime/retry.d.ts.map +1 -0
  97. package/dist/core/runtime/retry.js +60 -0
  98. package/dist/core/runtime/run-runtime.d.ts +93 -0
  99. package/dist/core/runtime/run-runtime.d.ts.map +1 -0
  100. package/dist/core/runtime/run-runtime.js +664 -0
  101. package/dist/core/runtime/runtime-catalog.d.ts +12 -0
  102. package/dist/core/runtime/runtime-catalog.d.ts.map +1 -0
  103. package/dist/core/runtime/runtime-catalog.js +89 -0
  104. package/dist/core/runtime/stream-rules.d.ts +9 -0
  105. package/dist/core/runtime/stream-rules.d.ts.map +1 -0
  106. package/dist/core/runtime/stream-rules.js +44 -0
  107. package/dist/core/runtime/title.d.ts +7 -0
  108. package/dist/core/runtime/title.d.ts.map +1 -0
  109. package/dist/core/runtime/title.js +114 -0
  110. package/dist/core/runtime/todo.d.ts +16 -0
  111. package/dist/core/runtime/todo.d.ts.map +1 -0
  112. package/dist/core/runtime/todo.js +40 -0
  113. package/dist/core/runtime/tool-filter.d.ts +12 -0
  114. package/dist/core/runtime/tool-filter.d.ts.map +1 -0
  115. package/dist/core/runtime/tool-filter.js +23 -0
  116. package/dist/core/runtime/tool-pruning.d.ts +29 -0
  117. package/dist/core/runtime/tool-pruning.d.ts.map +1 -0
  118. package/dist/core/runtime/tool-pruning.js +105 -0
  119. package/dist/core/session/input-history.d.ts +8 -0
  120. package/dist/core/session/input-history.d.ts.map +1 -0
  121. package/dist/core/session/input-history.js +41 -0
  122. package/dist/core/session/session-file.d.ts +84 -0
  123. package/dist/core/session/session-file.d.ts.map +1 -0
  124. package/dist/core/session/session-file.js +392 -0
  125. package/dist/core/session/session-loop.d.ts +33 -0
  126. package/dist/core/session/session-loop.d.ts.map +1 -0
  127. package/dist/core/session/session-loop.js +30 -0
  128. package/dist/core/settings/project-settings.d.ts +46 -0
  129. package/dist/core/settings/project-settings.d.ts.map +1 -0
  130. package/dist/core/settings/project-settings.js +85 -0
  131. package/dist/core/settings/workspace-context.d.ts +10 -0
  132. package/dist/core/settings/workspace-context.d.ts.map +1 -0
  133. package/dist/core/settings/workspace-context.js +48 -0
  134. package/dist/core/tools/agent-fs-like.d.ts +15 -0
  135. package/dist/core/tools/agent-fs-like.d.ts.map +1 -0
  136. package/dist/core/tools/agent-fs-like.js +4 -0
  137. package/dist/core/tools/ask-question.d.ts +5 -0
  138. package/dist/core/tools/ask-question.d.ts.map +1 -0
  139. package/dist/core/tools/ask-question.js +51 -0
  140. package/dist/core/tools/bash.d.ts +5 -0
  141. package/dist/core/tools/bash.d.ts.map +1 -0
  142. package/dist/core/tools/bash.js +109 -0
  143. package/dist/core/tools/eval.d.ts +10 -0
  144. package/dist/core/tools/eval.d.ts.map +1 -0
  145. package/dist/core/tools/eval.js +83 -0
  146. package/dist/core/tools/file-tools.d.ts +18 -0
  147. package/dist/core/tools/file-tools.d.ts.map +1 -0
  148. package/dist/core/tools/file-tools.js +249 -0
  149. package/dist/core/tools/glob.d.ts +5 -0
  150. package/dist/core/tools/glob.d.ts.map +1 -0
  151. package/dist/core/tools/glob.js +54 -0
  152. package/dist/core/tools/grep.d.ts +5 -0
  153. package/dist/core/tools/grep.d.ts.map +1 -0
  154. package/dist/core/tools/grep.js +61 -0
  155. package/dist/core/tools/index.d.ts +14 -0
  156. package/dist/core/tools/index.d.ts.map +1 -0
  157. package/dist/core/tools/index.js +14 -0
  158. package/dist/core/tools/ls-tree.d.ts +10 -0
  159. package/dist/core/tools/ls-tree.d.ts.map +1 -0
  160. package/dist/core/tools/ls-tree.js +177 -0
  161. package/dist/core/tools/mcp-mount.d.ts +63 -0
  162. package/dist/core/tools/mcp-mount.d.ts.map +1 -0
  163. package/dist/core/tools/mcp-mount.js +287 -0
  164. package/dist/core/tools/read-image.d.ts +6 -0
  165. package/dist/core/tools/read-image.d.ts.map +1 -0
  166. package/dist/core/tools/read-image.js +98 -0
  167. package/dist/core/tools/skill.d.ts +11 -0
  168. package/dist/core/tools/skill.d.ts.map +1 -0
  169. package/dist/core/tools/skill.js +65 -0
  170. package/dist/core/tools/skills.d.ts +10 -0
  171. package/dist/core/tools/skills.d.ts.map +1 -0
  172. package/dist/core/tools/skills.js +116 -0
  173. package/dist/core/tools/todo-store.d.ts +8 -0
  174. package/dist/core/tools/todo-store.d.ts.map +1 -0
  175. package/dist/core/tools/todo-store.js +100 -0
  176. package/dist/core/tools/todo.d.ts +8 -0
  177. package/dist/core/tools/todo.d.ts.map +1 -0
  178. package/dist/core/tools/todo.js +91 -0
  179. package/dist/core/tools/url-guard.d.ts +12 -0
  180. package/dist/core/tools/url-guard.d.ts.map +1 -0
  181. package/dist/core/tools/url-guard.js +58 -0
  182. package/dist/core/tools/web-ports-std.d.ts +6 -0
  183. package/dist/core/tools/web-ports-std.d.ts.map +1 -0
  184. package/dist/core/tools/web-ports-std.js +145 -0
  185. package/dist/core/tools/web-ports.d.ts +56 -0
  186. package/dist/core/tools/web-ports.d.ts.map +1 -0
  187. package/dist/core/tools/web-ports.js +62 -0
  188. package/dist/core/tools/workspace-sandbox.d.ts +20 -0
  189. package/dist/core/tools/workspace-sandbox.d.ts.map +1 -0
  190. package/dist/core/tools/workspace-sandbox.js +84 -0
  191. package/dist/core/workflow/subagent-registry.d.ts +40 -0
  192. package/dist/core/workflow/subagent-registry.d.ts.map +1 -0
  193. package/dist/core/workflow/subagent-registry.js +118 -0
  194. package/dist/core/workflow/workflow-evaluator.d.ts +23 -0
  195. package/dist/core/workflow/workflow-evaluator.d.ts.map +1 -0
  196. package/dist/core/workflow/workflow-evaluator.js +21 -0
  197. package/dist/core/workflow/workflow-executor.d.ts +108 -0
  198. package/dist/core/workflow/workflow-executor.d.ts.map +1 -0
  199. package/dist/core/workflow/workflow-executor.js +549 -0
  200. package/dist/core/workflow/workflow-tools.d.ts +50 -0
  201. package/dist/core/workflow/workflow-tools.d.ts.map +1 -0
  202. package/dist/core/workflow/workflow-tools.js +217 -0
  203. package/dist/index.d.ts +14 -0
  204. package/dist/index.d.ts.map +1 -0
  205. package/dist/index.js +13 -0
  206. package/dist/main.d.ts +9 -0
  207. package/dist/main.d.ts.map +1 -0
  208. package/dist/main.js +97 -0
  209. package/dist/modes/json-mode.d.ts +5 -0
  210. package/dist/modes/json-mode.d.ts.map +1 -0
  211. package/dist/modes/json-mode.js +46 -0
  212. package/dist/modes/print-mode.d.ts +20 -0
  213. package/dist/modes/print-mode.d.ts.map +1 -0
  214. package/dist/modes/print-mode.js +82 -0
  215. package/dist/modes/rpc/jsonl.d.ts +23 -0
  216. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  217. package/dist/modes/rpc/jsonl.js +88 -0
  218. package/dist/modes/rpc/rpc-mode.d.ts +18 -0
  219. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
  220. package/dist/modes/rpc/rpc-mode.js +326 -0
  221. package/dist/modes/tui/settings-overlay.d.ts +23 -0
  222. package/dist/modes/tui/settings-overlay.d.ts.map +1 -0
  223. package/dist/modes/tui/settings-overlay.js +136 -0
  224. package/dist/modes/tui/tui-commands-session.d.ts +3 -0
  225. package/dist/modes/tui/tui-commands-session.d.ts.map +1 -0
  226. package/dist/modes/tui/tui-commands-session.js +191 -0
  227. package/dist/modes/tui/tui-commands.d.ts +52 -0
  228. package/dist/modes/tui/tui-commands.d.ts.map +1 -0
  229. package/dist/modes/tui/tui-commands.js +373 -0
  230. package/dist/modes/tui/tui-components.d.ts +81 -0
  231. package/dist/modes/tui/tui-components.d.ts.map +1 -0
  232. package/dist/modes/tui/tui-components.js +248 -0
  233. package/dist/modes/tui/tui-format.d.ts +65 -0
  234. package/dist/modes/tui/tui-format.d.ts.map +1 -0
  235. package/dist/modes/tui/tui-format.js +246 -0
  236. package/dist/modes/tui/tui-frame-provider.d.ts +16 -0
  237. package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -0
  238. package/dist/modes/tui/tui-frame-provider.js +31 -0
  239. package/dist/modes/tui/tui-interactive.d.ts +16 -0
  240. package/dist/modes/tui/tui-interactive.d.ts.map +1 -0
  241. package/dist/modes/tui/tui-interactive.js +123 -0
  242. package/dist/modes/tui/tui-io.d.ts +12 -0
  243. package/dist/modes/tui/tui-io.d.ts.map +1 -0
  244. package/dist/modes/tui/tui-io.js +620 -0
  245. package/dist/modes/tui/tui-mode.d.ts +120 -0
  246. package/dist/modes/tui/tui-mode.d.ts.map +1 -0
  247. package/dist/modes/tui/tui-mode.js +391 -0
  248. package/dist/modes/tui/tui-render.d.ts +54 -0
  249. package/dist/modes/tui/tui-render.d.ts.map +1 -0
  250. package/dist/modes/tui/tui-render.js +399 -0
  251. package/dist/modes/tui/tui-slash.d.ts +12 -0
  252. package/dist/modes/tui/tui-slash.d.ts.map +1 -0
  253. package/dist/modes/tui/tui-slash.js +64 -0
  254. package/dist/modes/tui/tui-tool-render.d.ts +6 -0
  255. package/dist/modes/tui/tui-tool-render.d.ts.map +1 -0
  256. package/dist/modes/tui/tui-tool-render.js +79 -0
  257. package/dist/modes/tui/tui-transcript-reconciler.d.ts +25 -0
  258. package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -0
  259. package/dist/modes/tui/tui-transcript-reconciler.js +97 -0
  260. package/dist/modes/tui/view-state.d.ts +67 -0
  261. package/dist/modes/tui/view-state.d.ts.map +1 -0
  262. package/dist/modes/tui/view-state.js +353 -0
  263. package/dist/prompts/agents/subagent.md +26 -0
  264. package/dist/prompts/system/base.md +20 -0
  265. package/dist/prompts/system/memory.md +23 -0
  266. package/dist/prompts/system/safety.md +12 -0
  267. package/dist/protocol/index.d.ts +5 -0
  268. package/dist/protocol/index.d.ts.map +1 -0
  269. package/dist/protocol/index.js +2 -0
  270. package/dist/protocol/mapping.d.ts +25 -0
  271. package/dist/protocol/mapping.d.ts.map +1 -0
  272. package/dist/protocol/mapping.js +115 -0
  273. package/dist/protocol/transport.d.ts +1396 -0
  274. package/dist/protocol/transport.d.ts.map +1 -0
  275. package/dist/protocol/transport.js +170 -0
  276. package/package.json +49 -0
@@ -0,0 +1,620 @@
1
+ import { applyBackgroundToLine, CombinedAutocompleteProvider, Container, Editor, Loader, matchesKey, ProcessTerminal, SelectList, Text, TUI, truncateToWidth, } from "@chengchenccc/tui";
2
+ import { appendInputHistory, loadInputHistory, saveInputHistory, } from "../../core/session/input-history.js";
3
+ import { SettingsOverlay } from "./settings-overlay.js";
4
+ import { HistorySearchOverlay, OmaTranscriptContainer, PickerOverlay } from "./tui-components.js";
5
+ import { EDITOR_THEME, relativeTime, WELCOME_TIPS } from "./tui-format.js";
6
+ import { createOmaFrameProvider } from "./tui-frame-provider.js";
7
+ import { TuiRenderShell } from "./tui-render.js";
8
+ /** Pre-order branch-tree rows with git-graph prefixes (pi tree-selector
9
+ * semantics): indent grows only at branch points, single-child chains stay
10
+ * flat, ancestors leave "│" rails at their fork columns. */
11
+ export function layoutBranchTree(nodes) {
12
+ const byId = new Map(nodes.map((n) => [n.id, n]));
13
+ const childrenOf = new Map();
14
+ const roots = [];
15
+ for (const n of nodes) {
16
+ if (n.parentId && byId.has(n.parentId)) {
17
+ const arr = childrenOf.get(n.parentId) ?? [];
18
+ arr.push(n);
19
+ childrenOf.set(n.parentId, arr);
20
+ }
21
+ else {
22
+ roots.push(n);
23
+ }
24
+ }
25
+ const multipleRoots = roots.length > 1;
26
+ const rows = [];
27
+ const stack = [];
28
+ for (let i = roots.length - 1; i >= 0; i--) {
29
+ stack.push([
30
+ roots[i],
31
+ multipleRoots ? 1 : 0,
32
+ multipleRoots,
33
+ multipleRoots,
34
+ i === roots.length - 1,
35
+ [],
36
+ multipleRoots,
37
+ ]);
38
+ }
39
+ while (stack.length > 0) {
40
+ const [node, lane, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
41
+ const displayIndent = multipleRoots ? Math.max(0, lane - 1) : lane;
42
+ const connectorLevel = showConnector && !isVirtualRootChild ? displayIndent - 1 : -1;
43
+ const cells = [];
44
+ for (let level = 0; level < displayIndent; level++) {
45
+ const gutter = gutters.find((g) => g.position === level);
46
+ if (level === connectorLevel)
47
+ cells.push(isLast ? "└─ " : "├─ ");
48
+ else
49
+ cells.push(gutter?.show ? "│ " : " ");
50
+ }
51
+ rows.push({ node, prefix: cells.join("") });
52
+ const children = childrenOf.get(node.id) ?? [];
53
+ const multipleChildren = children.length > 1;
54
+ const childLane = multipleChildren || (justBranched && lane > 0) ? lane + 1 : lane;
55
+ const connectorDisplayed = showConnector && !isVirtualRootChild;
56
+ const childGutters = connectorDisplayed
57
+ ? [...gutters, { position: Math.max(0, displayIndent - 1), show: !isLast }]
58
+ : gutters;
59
+ for (let i = children.length - 1; i >= 0; i--) {
60
+ stack.push([
61
+ children[i],
62
+ childLane,
63
+ multipleChildren,
64
+ multipleChildren,
65
+ i === children.length - 1,
66
+ childGutters,
67
+ false,
68
+ ]);
69
+ }
70
+ }
71
+ return rows;
72
+ }
73
+ /** One-shot SelectList overlay; resolves the picked value or null on esc. */
74
+ function pickOne(tui, title, items) {
75
+ const { promise, resolve } = Promise.withResolvers();
76
+ const list = new SelectList([...items], 10, EDITOR_THEME.selectList, {
77
+ minPrimaryColumnWidth: 6,
78
+ maxPrimaryColumnWidth: 42,
79
+ });
80
+ const overlayBox = new PickerOverlay(new Text(title, 0, 0), list);
81
+ const overlay = tui.showOverlay(overlayBox, { width: "70%", anchor: "center" });
82
+ list.onSelect = (item) => {
83
+ overlay.hide();
84
+ resolve(item.value);
85
+ };
86
+ list.onCancel = () => {
87
+ overlay.hide();
88
+ resolve(null);
89
+ };
90
+ return promise;
91
+ }
92
+ /** Notice-only overlay; resolves true when dismissed, null is not needed. */
93
+ function pickNotice(tui, title) {
94
+ const { promise, resolve } = Promise.withResolvers();
95
+ const list = new SelectList([{ value: "ok", label: "ok" }], 1, EDITOR_THEME.selectList, {
96
+ minPrimaryColumnWidth: 2,
97
+ maxPrimaryColumnWidth: 4,
98
+ });
99
+ const overlayBox = new PickerOverlay(new Text(title, 0, 0), list);
100
+ const overlay = tui.showOverlay(overlayBox, { width: "60%", anchor: "center" });
101
+ list.onSelect = () => {
102
+ overlay.hide();
103
+ resolve(true);
104
+ };
105
+ list.onCancel = () => {
106
+ overlay.hide();
107
+ resolve(true);
108
+ };
109
+ return promise;
110
+ }
111
+ export function createTerminalIo(terminal = new ProcessTerminal(), workspaceRoot = process.cwd()) {
112
+ const tui = new TUI(terminal);
113
+ const headerContainer = new Container();
114
+ const transcript = new OmaTranscriptContainer();
115
+ const statusContainer = new Container();
116
+ const welcomeTip = WELCOME_TIPS[Math.floor(Math.random() * WELCOME_TIPS.length)] ?? "";
117
+ const shell = new TuiRenderShell(tui, headerContainer, transcript, statusContainer, workspaceRoot, welcomeTip);
118
+ const editorTheme = {
119
+ ...EDITOR_THEME,
120
+ topBorder: (width) => {
121
+ if (!shell.statusLineText)
122
+ return EDITOR_THEME.borderColor("─".repeat(width));
123
+ // Re-fit the powerline status bar to the current width. The raw string
124
+ // is width-independent; addStatusBar must not pre-truncate it, or a
125
+ // terminal resize leaves a stale-width status line above the editor.
126
+ const line = truncateToWidth(shell.statusLineText, width, "");
127
+ return applyBackgroundToLine(line, width, (s) => `\u001b[48;5;234m${s}\u001b[0m`);
128
+ },
129
+ };
130
+ const editor = new Editor(tui, editorTheme);
131
+ shell.renderHeader();
132
+ let pending = null;
133
+ let busy = false;
134
+ let liveHandler = null;
135
+ let liveCommandHandler = null;
136
+ let commandHandler = null;
137
+ let focusHandler = null;
138
+ let loader = null;
139
+ let busySince = 0;
140
+ let busySeconds = 0;
141
+ let elapsedTimer;
142
+ let focused = true;
143
+ // Persistent prompt history (pi's HistoryStorage): loaded newest-first,
144
+ // fed to the editor (up/down recall, in-memory cap 100) and appended on
145
+ // every submit — idle prompts AND live steers both pass through
146
+ // editor.onSubmit, the single choke point. Drained follow-ups were already
147
+ // recorded at steer time.
148
+ let historyEntries = loadInputHistory();
149
+ for (const prompt of historyEntries.slice(0, 100).reverse()) {
150
+ editor.addToHistory(prompt);
151
+ }
152
+ // Idle Ctrl-C quits only on a SECOND press within 2s (a stray press must
153
+ // not kill the session); busy Ctrl-C stays single-press because aborting
154
+ // a run is not destructive. Ctrl-D remains an instant quit.
155
+ let quitArmed = false;
156
+ let quitHint = null;
157
+ let quitTimer;
158
+ let escArmed = false;
159
+ let escTimer;
160
+ let escHint = null;
161
+ function startElapsedTimer() {
162
+ clearInterval(elapsedTimer);
163
+ elapsedTimer = setInterval(() => {
164
+ if (!busy)
165
+ return;
166
+ busySeconds = Math.floor((Date.now() - busySince) / 1000);
167
+ shell.setBusySeconds(busySeconds);
168
+ shell.updateWorkingMessage();
169
+ }, 1000);
170
+ }
171
+ /** Derive a short human-readable summary for the currently streaming
172
+ * activity (omp's intent/working message). Prefer a live tool intent;
173
+ * when the model is composing a response, surface the first line of its
174
+ * thinking or output so the composer is never just "working…". */
175
+ function dismissQuitHint() {
176
+ quitArmed = false;
177
+ clearTimeout(quitTimer);
178
+ if (quitHint) {
179
+ statusContainer.removeChild(quitHint);
180
+ quitHint = null;
181
+ }
182
+ }
183
+ function armQuit() {
184
+ quitArmed = true;
185
+ quitHint = new Text("\u001b[33m press ctrl+c again to quit\u001b[0m", 0, 0);
186
+ statusContainer.addChild(quitHint);
187
+ tui.requestRender();
188
+ quitTimer = setTimeout(() => {
189
+ dismissQuitHint();
190
+ tui.requestRender();
191
+ }, 2_000);
192
+ }
193
+ /** Idle esc-esc opens the branch tree. A single esc only arms so the esc
194
+ * that CLOSES an overlay can never immediately re-summon it. */
195
+ function dismissEscArm() {
196
+ escArmed = false;
197
+ clearTimeout(escTimer);
198
+ if (escHint) {
199
+ statusContainer.removeChild(escHint);
200
+ escHint = null;
201
+ }
202
+ }
203
+ function armEsc() {
204
+ dismissEscArm();
205
+ escArmed = true;
206
+ escHint = new Text("\u001b[2m press esc again for branch tree\u001b[0m", 0, 0);
207
+ statusContainer.addChild(escHint);
208
+ tui.requestRender();
209
+ escTimer = setTimeout(() => {
210
+ dismissEscArm();
211
+ tui.requestRender();
212
+ }, 2_000);
213
+ }
214
+ function recordHistory(prompt) {
215
+ const next = appendInputHistory(historyEntries, prompt);
216
+ if (next === historyEntries)
217
+ return;
218
+ historyEntries = next;
219
+ saveInputHistory(historyEntries);
220
+ editor.addToHistory(prompt);
221
+ }
222
+ editor.onSubmit = (text) => {
223
+ if (busy) {
224
+ const trimmed = text.trim();
225
+ // Slash commands execute live (pi's LiveCommandController) instead of
226
+ // being steered into the model as literal text; anything else steers
227
+ // immediately — the session loop queues a rejected steer as the next
228
+ // Run's input, so nothing is lost either way.
229
+ if (!trimmed)
230
+ return;
231
+ if (trimmed.startsWith("/")) {
232
+ if (liveCommandHandler)
233
+ liveCommandHandler(trimmed);
234
+ return;
235
+ }
236
+ recordHistory(trimmed);
237
+ if (liveHandler)
238
+ liveHandler(trimmed);
239
+ return;
240
+ }
241
+ dismissEscArm();
242
+ if (!pending)
243
+ return;
244
+ const resolve = pending;
245
+ pending = null;
246
+ if (text === "/exit" || text === "/quit") {
247
+ resolve(null);
248
+ return;
249
+ }
250
+ recordHistory(text);
251
+ resolve(text);
252
+ };
253
+ // Esc/ctrl+t/ctrl+o are intercepted before the editor sees them. Esc
254
+ // aborts a live run (pi's app.interrupt); ctrl+t and ctrl+o toggle the
255
+ // thinking-block and tool-detail views globally.
256
+ // Last rendered state: resize/setHeader rebuild the transcript from it.
257
+ tui.addInputListener((data) => {
258
+ // Focus reporting (CSI 1004): ESC[I focused, ESC[O unfocused.
259
+ if (data === "\x1b[I") {
260
+ focused = true;
261
+ focusHandler?.(true);
262
+ return { consume: true };
263
+ }
264
+ if (data === "\x1b[O") {
265
+ focused = false;
266
+ focusHandler?.(false);
267
+ return { consume: true };
268
+ }
269
+ if (matchesKey(data, "escape")) {
270
+ // An overlay must own Esc: abort/forkTree must not steal the key while
271
+ // the user is trying to cancel the overlay, or it becomes impossible to
272
+ // close (e.g. branch-tree picker).
273
+ if (tui.hasOverlay())
274
+ return undefined;
275
+ if (busy) {
276
+ if (commandHandler)
277
+ commandHandler("abort");
278
+ return { consume: true };
279
+ }
280
+ if (!editor.isShowingAutocomplete() && commandHandler) {
281
+ // esc-esc summons; the first esc only arms (quit-arm pattern) so an
282
+ // extra esc while dismissing overlays never reopens the tree.
283
+ if (escArmed) {
284
+ dismissEscArm();
285
+ commandHandler("forkTree");
286
+ }
287
+ else {
288
+ armEsc();
289
+ }
290
+ return { consume: true };
291
+ }
292
+ }
293
+ if (matchesKey(data, "ctrl+t")) {
294
+ if (commandHandler)
295
+ commandHandler("toggleThinking");
296
+ return { consume: true };
297
+ }
298
+ if (matchesKey(data, "ctrl+o")) {
299
+ if (commandHandler)
300
+ commandHandler("toggleToolDetail");
301
+ return { consume: true };
302
+ }
303
+ if (matchesKey(data, "ctrl+p")) {
304
+ if (commandHandler)
305
+ commandHandler("pickModel");
306
+ return { consume: true };
307
+ }
308
+ // ctrl+r: search the persistent prompt history (pi's history search).
309
+ if (matchesKey(data, "ctrl+r")) {
310
+ openHistorySearch();
311
+ return { consume: true };
312
+ }
313
+ // ctrl+c aborts the live run when busy and quits when idle.
314
+ if (matchesKey(data, "ctrl+c")) {
315
+ if (busy) {
316
+ if (commandHandler)
317
+ commandHandler("abort");
318
+ }
319
+ else if (pending) {
320
+ // Idle: the second press within 2s quits; the first just arms.
321
+ if (quitArmed) {
322
+ dismissQuitHint();
323
+ const resolve = pending;
324
+ pending = null;
325
+ resolve(null);
326
+ }
327
+ else {
328
+ armQuit();
329
+ }
330
+ }
331
+ tui.requestRender();
332
+ return { consume: true };
333
+ }
334
+ return undefined;
335
+ });
336
+ function openHistorySearch() {
337
+ if (historyEntries.length === 0) {
338
+ const hint = new Text("\u001b[2m history is empty\u001b[0m", 0, 0);
339
+ statusContainer.addChild(hint);
340
+ tui.requestRender();
341
+ setTimeout(() => {
342
+ statusContainer.removeChild(hint);
343
+ tui.requestRender();
344
+ }, 1_500);
345
+ return;
346
+ }
347
+ const { promise, resolve } = Promise.withResolvers();
348
+ const overlayBox = new HistorySearchOverlay(new Text(" history search — filter, enter, esc", 0, 0), historyEntries, EDITOR_THEME.selectList, (value) => {
349
+ overlay.hide();
350
+ resolve(value);
351
+ }, () => {
352
+ overlay.hide();
353
+ resolve(null);
354
+ });
355
+ const overlay = tui.showOverlay(overlayBox, { width: "70%", anchor: "center" });
356
+ void promise.then((value) => {
357
+ if (value !== null)
358
+ editor.setText(value);
359
+ tui.requestRender();
360
+ });
361
+ }
362
+ tui.setFrameProvider(createOmaFrameProvider({
363
+ headerContainer,
364
+ transcript,
365
+ statusContainer,
366
+ editor,
367
+ shell,
368
+ }));
369
+ tui.addChild(headerContainer);
370
+ tui.addChild(transcript);
371
+ tui.addChild(statusContainer);
372
+ tui.addChild(editor);
373
+ tui.setFocus(editor);
374
+ tui.start();
375
+ tui.terminal.write("\x1b[?1004h");
376
+ return {
377
+ render(state) {
378
+ shell.render(state);
379
+ },
380
+ waitForInput() {
381
+ return new Promise((resolve) => {
382
+ pending = resolve;
383
+ });
384
+ },
385
+ setBusy(next) {
386
+ busy = next;
387
+ // The animated status line: a braille spinner + "working (Ns)" while
388
+ // a run is live, removed when it settles. Loader drives its own timer
389
+ // and calls requestRender on every frame; the elapsed timer ticks the
390
+ // seconds.
391
+ statusContainer.clear();
392
+ if (next) {
393
+ shell.addStatusBar();
394
+ shell.setBusy(true, null, 0);
395
+ busySince = Date.now();
396
+ busySeconds = 0;
397
+ shell.setBusySeconds(0);
398
+ loader = new Loader(tui, (s) => `\u001b[36m${s}\u001b[0m`, (s) => `\u001b[2m${s}\u001b[0m`, "working… (esc to abort)");
399
+ loader.start();
400
+ statusContainer.addChild(loader);
401
+ shell.setBusy(true, loader, 0);
402
+ shell.updateWorkingMessage();
403
+ startElapsedTimer();
404
+ }
405
+ else {
406
+ clearInterval(elapsedTimer);
407
+ busySeconds = 0;
408
+ if (loader) {
409
+ loader.stop();
410
+ loader = null;
411
+ }
412
+ shell.setBusy(false, null, 0);
413
+ shell.renderIdleFooter();
414
+ }
415
+ tui.requestRender();
416
+ },
417
+ onLiveInput(handler) {
418
+ liveHandler = handler;
419
+ },
420
+ onLiveCommand(handler) {
421
+ liveCommandHandler = handler;
422
+ },
423
+ onCommand(handler) {
424
+ commandHandler = handler;
425
+ },
426
+ onFocus(handler) {
427
+ focusHandler = handler;
428
+ },
429
+ setSlashCommands(commands) {
430
+ // Registers slash-command autocomplete; also enables @-file completion
431
+ // over the workspace as a side effect of the combined provider.
432
+ editor.setAutocompleteProvider(new CombinedAutocompleteProvider([...commands], workspaceRoot));
433
+ },
434
+ pickSession(sessions) {
435
+ const { promise, resolve } = Promise.withResolvers();
436
+ const items = sessions.map((s) => {
437
+ const base = s.title ?? (s.preview || s.id.slice(0, 8));
438
+ const fork = s.forkOf ? ` \u2442 ${s.forkOf.slice(0, 8)}` : "";
439
+ const workspace = s.workspace ? ` [${s.workspace}]` : "";
440
+ return {
441
+ value: s.id,
442
+ label: relativeTime(s.modifiedAt),
443
+ description: `${base}${fork}${workspace}`,
444
+ };
445
+ });
446
+ const list = new SelectList(items, 10, EDITOR_THEME.selectList, {
447
+ minPrimaryColumnWidth: 6,
448
+ maxPrimaryColumnWidth: 8,
449
+ });
450
+ const overlayBox = new PickerOverlay(new Text(" resume session — select, enter, esc", 0, 0), list);
451
+ const overlay = tui.showOverlay(overlayBox, { width: "60%", anchor: "center" });
452
+ list.onSelect = (item) => {
453
+ overlay.hide();
454
+ resolve(item.value);
455
+ };
456
+ list.onCancel = () => {
457
+ overlay.hide();
458
+ resolve(null);
459
+ };
460
+ return promise;
461
+ },
462
+ pickModel(models) {
463
+ const { promise, resolve } = Promise.withResolvers();
464
+ const items = models.map((m) => ({
465
+ value: m.id,
466
+ label: m.id,
467
+ description: m.description,
468
+ }));
469
+ const list = new SelectList(items, 10, EDITOR_THEME.selectList, {
470
+ minPrimaryColumnWidth: 12,
471
+ maxPrimaryColumnWidth: 32,
472
+ });
473
+ const overlayBox = new PickerOverlay(new Text(" pick model — select, enter, esc", 0, 0), list);
474
+ const overlay = tui.showOverlay(overlayBox, { width: "60%", anchor: "center" });
475
+ list.onSelect = (item) => {
476
+ overlay.hide();
477
+ resolve(item.value);
478
+ };
479
+ list.onCancel = () => {
480
+ overlay.hide();
481
+ resolve(null);
482
+ };
483
+ return promise;
484
+ },
485
+ confirmApproval(req) {
486
+ const { promise, resolve } = Promise.withResolvers();
487
+ const list = new SelectList([
488
+ { value: "allow", label: "allow", description: "run the tool" },
489
+ { value: "deny", label: "deny", description: "block with an error result" },
490
+ ], 2, EDITOR_THEME.selectList, { minPrimaryColumnWidth: 6, maxPrimaryColumnWidth: 8 });
491
+ const overlayBox = new PickerOverlay(new Text(` approve ${req.toolName}${req.reason ? ` — ${req.reason}` : ""} — select, enter, esc`, 0, 0), list);
492
+ const overlay = tui.showOverlay(overlayBox, { width: "60%", anchor: "center" });
493
+ list.onSelect = (item) => {
494
+ overlay.hide();
495
+ resolve(item.value === "allow" ? "allow" : "deny");
496
+ };
497
+ list.onCancel = () => {
498
+ overlay.hide();
499
+ resolve(null);
500
+ };
501
+ return promise;
502
+ },
503
+ askQuestions(input) {
504
+ // ponytail: TUI v1 fully supports single-select questions via the
505
+ // SelectList overlay; multi/text degrade to a notice overlay resolving
506
+ // null (tool fails closed). Upgrade path: an Input-based form overlay
507
+ // (packages/tui Input is Focusable) for text + checkbox rows for multi.
508
+ return (async () => {
509
+ const answers = [];
510
+ for (const q of input.questions) {
511
+ if (q.kind === "text" || q.multi) {
512
+ const unsupported = await pickNotice(tui, ` ${q.question} — ${q.kind === "text" ? "text" : "multi-select"} input not supported in TUI yet — esc`);
513
+ if (!unsupported)
514
+ return null;
515
+ continue;
516
+ }
517
+ const options = q.options ?? [];
518
+ const picked = await pickOne(tui, ` ${q.question} — select, enter, esc${q.header ? ` — ${q.header}` : ""}`, options.map((o) => ({
519
+ value: o.value,
520
+ label: o.label + (q.recommended === o.value ? " (Recommended)" : ""),
521
+ description: o.description,
522
+ })));
523
+ if (picked === null)
524
+ return null;
525
+ answers.push({ id: q.id, selectedValues: [picked] });
526
+ }
527
+ return { answers };
528
+ })();
529
+ },
530
+ pickForkPoint(points) {
531
+ const { promise, resolve } = Promise.withResolvers();
532
+ const items = points.map((p) => ({
533
+ value: String(p.ordinal),
534
+ label: `#${p.ordinal}`,
535
+ description: p.text,
536
+ }));
537
+ const list = new SelectList(items, 10, EDITOR_THEME.selectList, {
538
+ minPrimaryColumnWidth: 4,
539
+ maxPrimaryColumnWidth: 6,
540
+ });
541
+ const overlayBox = new PickerOverlay(new Text(" fork from message — select, enter, esc", 0, 0), list);
542
+ const overlay = tui.showOverlay(overlayBox, { width: "70%", anchor: "center" });
543
+ list.onSelect = (item) => {
544
+ overlay.hide();
545
+ resolve(Number(item.value));
546
+ };
547
+ list.onCancel = () => {
548
+ overlay.hide();
549
+ resolve(null);
550
+ };
551
+ return promise;
552
+ },
553
+ pickBranchTree(nodes) {
554
+ const { promise, resolve } = Promise.withResolvers();
555
+ // Git-graph layout (pi tree-selector): a node's indent only grows at
556
+ // branch points — single-child chains stay flat, so a linear session
557
+ // renders as one column instead of one-indent-per-message. Rails:
558
+ // "│" at ancestor fork columns, "├─/└─" for children of a fork.
559
+ const items = layoutBranchTree(nodes)
560
+ .slice(0, 200)
561
+ .map(({ node: n, prefix }) => {
562
+ const roleColor = n.role === "user" ? "\u001b[36m" : n.role === "assistant" ? "\u001b[32m" : "\u001b[2m";
563
+ const ordinal = n.ordinal !== undefined ? ` #${n.ordinal}` : "";
564
+ return {
565
+ value: n.id,
566
+ label: `${prefix}${roleColor}${n.role}${ordinal}\u001b[0m`,
567
+ description: n.text.replace(/\s+/g, " ").slice(0, 60),
568
+ };
569
+ });
570
+ const list = new SelectList(items, 12, EDITOR_THEME.selectList, {
571
+ minPrimaryColumnWidth: 14,
572
+ maxPrimaryColumnWidth: 32,
573
+ });
574
+ const overlayBox = new PickerOverlay(new Text(" fork from branch node — select, enter, esc-esc to open", 0, 0), list);
575
+ const overlay = tui.showOverlay(overlayBox, { width: "75%", anchor: "center" });
576
+ list.onSelect = (item) => {
577
+ overlay.hide();
578
+ resolve(item.value);
579
+ };
580
+ list.onCancel = () => {
581
+ overlay.hide();
582
+ resolve(null);
583
+ };
584
+ return promise;
585
+ },
586
+ editSettings(settings) {
587
+ const { promise, resolve } = Promise.withResolvers();
588
+ const box = new SettingsOverlay(settings, () => {
589
+ handle.hide();
590
+ resolve(box.getSettings());
591
+ });
592
+ const handle = tui.showOverlay(box, { width: "70%", anchor: "center" });
593
+ return promise;
594
+ },
595
+ setHeader(info) {
596
+ shell.setHeader(info.model ?? "", info.sessionId ?? "", info.title ?? "", info.context);
597
+ },
598
+ setInputText(text) {
599
+ editor.setText(text);
600
+ tui.requestRender();
601
+ },
602
+ isFocused() {
603
+ return focused;
604
+ },
605
+ notify() {
606
+ tui.terminal.write("\x07");
607
+ },
608
+ close() {
609
+ clearInterval(elapsedTimer);
610
+ clearTimeout(quitTimer);
611
+ clearTimeout(escTimer);
612
+ dismissQuitHint();
613
+ dismissEscArm();
614
+ if (loader)
615
+ loader.stop();
616
+ tui.terminal.write("\x1b[?1004l");
617
+ tui.stop();
618
+ },
619
+ };
620
+ }