dexterm-cli 0.1.6 → 0.2.0

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 (395) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/spectator.js +3 -1
  3. package/dist/packages/agents/src/story.js +11 -2
  4. package/dist/packages/agents/src/tiers.js +36 -2
  5. package/dist/packages/agents/src/tools.js +35 -2
  6. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  7. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  8. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  9. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  10. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  11. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  12. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  13. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  14. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  15. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  16. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  17. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  18. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  19. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  20. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  21. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  22. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  23. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  24. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  25. package/dist/packages/autotrade/src/account.js +74 -0
  26. package/dist/packages/autotrade/src/backtest.js +358 -0
  27. package/dist/packages/autotrade/src/basis-text.js +32 -0
  28. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  29. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  30. package/dist/packages/autotrade/src/chat/config.js +95 -0
  31. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  32. package/dist/packages/autotrade/src/chat/control.js +104 -0
  33. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  34. package/dist/packages/autotrade/src/chat/format.js +71 -0
  35. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  36. package/dist/packages/autotrade/src/chat/index.js +18 -0
  37. package/dist/packages/autotrade/src/chat/notify.js +302 -0
  38. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  39. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  40. package/dist/packages/autotrade/src/chat/secrets.js +106 -0
  41. package/dist/packages/autotrade/src/chat/service.js +251 -0
  42. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  43. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  44. package/dist/packages/autotrade/src/engine.js +401 -0
  45. package/dist/packages/autotrade/src/execution-port.js +97 -0
  46. package/dist/packages/autotrade/src/executor-health.js +217 -0
  47. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  48. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  49. package/dist/packages/autotrade/src/fleet.js +642 -0
  50. package/dist/packages/autotrade/src/forward.js +355 -0
  51. package/dist/packages/autotrade/src/index.js +72 -0
  52. package/dist/packages/autotrade/src/journey.js +477 -0
  53. package/dist/packages/autotrade/src/ledger.js +108 -0
  54. package/dist/packages/autotrade/src/limits.js +682 -0
  55. package/dist/packages/autotrade/src/live-source.js +182 -0
  56. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  57. package/dist/packages/autotrade/src/monitor.js +190 -0
  58. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  59. package/dist/packages/autotrade/src/outbox.js +394 -0
  60. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  61. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  62. package/dist/packages/autotrade/src/real-mode.js +688 -0
  63. package/dist/packages/autotrade/src/receipts.js +330 -0
  64. package/dist/packages/autotrade/src/replay.js +198 -0
  65. package/dist/packages/autotrade/src/report.js +168 -0
  66. package/dist/packages/autotrade/src/runner-config.js +99 -0
  67. package/dist/packages/autotrade/src/runner.js +923 -0
  68. package/dist/packages/autotrade/src/safety.js +243 -0
  69. package/dist/packages/autotrade/src/service-guard.js +125 -0
  70. package/dist/packages/autotrade/src/service-install.js +213 -0
  71. package/dist/packages/autotrade/src/service-recover.js +232 -0
  72. package/dist/packages/autotrade/src/service.js +289 -0
  73. package/dist/packages/autotrade/src/session.js +269 -0
  74. package/dist/packages/autotrade/src/sessions.js +503 -0
  75. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  76. package/dist/packages/autotrade/src/signal.js +196 -0
  77. package/dist/packages/autotrade/src/store.js +250 -0
  78. package/dist/packages/autotrade/src/strategy.js +258 -0
  79. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  80. package/dist/packages/autotrade/src/yolo.js +140 -0
  81. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  82. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  83. package/dist/packages/cli/src/ai-cmd.js +412 -0
  84. package/dist/packages/cli/src/analyst/backend.js +83 -15
  85. package/dist/packages/cli/src/analyst/codex.js +15 -9
  86. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  87. package/dist/packages/cli/src/analyst/session.js +5 -2
  88. package/dist/packages/cli/src/app-help-gen.js +56 -8
  89. package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
  90. package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
  91. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  92. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  93. package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
  94. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  95. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  96. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  97. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  98. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  99. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  100. package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
  101. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  102. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  103. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  104. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  105. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  106. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  107. package/dist/packages/cli/src/autotrade-cmd.js +214 -0
  108. package/dist/packages/cli/src/display.js +156 -17
  109. package/dist/packages/cli/src/doctor.js +62 -3
  110. package/dist/packages/cli/src/export.js +10 -2
  111. package/dist/packages/cli/src/index.js +1 -1
  112. package/dist/packages/cli/src/ledger-cmd.js +290 -0
  113. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  114. package/dist/packages/cli/src/live/index.js +23 -89
  115. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  116. package/dist/packages/cli/src/live/trade.js +12 -457
  117. package/dist/packages/cli/src/live/watchlist.js +1 -1
  118. package/dist/packages/cli/src/main.js +147 -190
  119. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  120. package/dist/packages/cli/src/plan-cmd.js +1 -1
  121. package/dist/packages/cli/src/pnl-app.js +31 -21
  122. package/dist/packages/cli/src/pnl-card.js +162 -21
  123. package/dist/packages/cli/src/popup.js +22 -6
  124. package/dist/packages/cli/src/record-cmd.js +141 -20
  125. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  126. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  127. package/dist/packages/cli/src/screens/analyst.js +3 -2
  128. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  129. package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
  130. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  131. package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
  132. package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
  133. package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
  134. package/dist/packages/cli/src/screens/brain.js +37 -11
  135. package/dist/packages/cli/src/screens/overlay.js +5 -2
  136. package/dist/packages/cli/src/screens/plans.js +5 -17
  137. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  138. package/dist/packages/cli/src/screens/positions.js +6 -28
  139. package/dist/packages/cli/src/screens/ticket.js +38 -205
  140. package/dist/packages/cli/src/screens/token.js +3 -3
  141. package/dist/packages/cli/src/state.js +25 -19
  142. package/dist/packages/cli/src/telegram-cmd.js +454 -0
  143. package/dist/packages/cli/src/trade.js +3 -128
  144. package/dist/packages/cli/src/version.js +1 -1
  145. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  146. package/dist/packages/cli/src/wallet-command.js +8 -11
  147. package/dist/packages/cli/src/widgets.js +0 -12
  148. package/dist/packages/custom/src/config.js +20 -2
  149. package/dist/packages/custom/src/schema.js +35 -10
  150. package/dist/packages/custom/src/workspace.js +3 -3
  151. package/dist/packages/data/src/basis.js +191 -0
  152. package/dist/packages/data/src/detail.js +53 -2
  153. package/dist/packages/data/src/index.js +0 -14
  154. package/dist/packages/data/src/store.js +14 -5
  155. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  156. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  157. package/dist/packages/data/src/trade/bridge.js +4 -102
  158. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  159. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  160. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  161. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  162. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  163. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  164. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  165. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  166. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  167. package/dist/packages/data/src/trade/plans/index.js +0 -1
  168. package/dist/packages/data/src/trade/plans/service.js +3 -27
  169. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  170. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  171. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  172. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  173. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  174. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  175. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  176. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  177. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  178. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  179. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  180. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  181. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  182. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  183. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  184. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  185. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  186. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
  187. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  188. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  189. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  190. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  191. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  192. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  193. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  194. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  195. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  196. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  201. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  231. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  232. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  243. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  244. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  247. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  249. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  251. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  254. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  255. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  256. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  257. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  288. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  290. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  291. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  292. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  293. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  295. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  297. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  298. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  299. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  301. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  304. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  305. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  306. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  307. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  308. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  309. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  310. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  311. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  312. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  313. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  314. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  315. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  316. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  317. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  318. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  319. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  320. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  321. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  322. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  323. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  324. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  325. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  326. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  327. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  328. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  329. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  330. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  331. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  332. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  333. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  334. package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
  335. package/dist/packages/mcp/src/app-help.js +1 -1
  336. package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
  337. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  338. package/dist/packages/mcp/src/engine/config.js +159 -0
  339. package/dist/packages/mcp/src/engine/index.js +10 -0
  340. package/dist/packages/mcp/src/engine/loop.js +101 -0
  341. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  342. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  343. package/dist/packages/mcp/src/engine/probe.js +110 -0
  344. package/dist/packages/mcp/src/engine/reason.js +35 -0
  345. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  346. package/dist/packages/mcp/src/index.js +7 -1
  347. package/dist/packages/mcp/src/ledger-tools.js +494 -0
  348. package/dist/packages/mcp/src/paper-agent.js +2 -0
  349. package/dist/packages/mcp/src/server.js +138 -19
  350. package/dist/packages/packs/src/backtest.js +3 -1
  351. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  352. package/dist/packages/popup/src/pages/chart.css +9 -0
  353. package/dist/packages/popup/src/pages/chart.html +3 -1
  354. package/dist/packages/popup/src/pages/chart.js +104 -7
  355. package/dist/packages/popup/src/protocol.js +2 -0
  356. package/dist/packages/popup/src/server.js +4 -0
  357. package/dist/packages/recorder/src/index.js +2 -0
  358. package/dist/packages/recorder/src/recorder.js +63 -6
  359. package/dist/packages/recorder/src/retention.js +100 -0
  360. package/dist/packages/recorder/src/schema.js +34 -1
  361. package/dist/packages/recorder/src/ticks.js +237 -0
  362. package/dist/packages/tui/src/index.js +1 -0
  363. package/dist/packages/tui/src/keymap.js +28 -1
  364. package/dist/packages/tui/src/markers.js +219 -0
  365. package/dist/packages/tui/src/widgets.js +149 -2
  366. package/package.json +2 -1
  367. package/dist/packages/data/src/trade/caps.js +0 -323
  368. package/dist/packages/data/src/trade/executor.js +0 -933
  369. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  370. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  371. package/dist/packages/data/src/trade/signer/client.js +0 -197
  372. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  373. package/dist/packages/data/src/trade/signer/host.js +0 -97
  374. package/dist/packages/data/src/trade/signer/main.js +0 -121
  375. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  376. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  377. package/dist/packages/data/src/trade/signer/service.js +0 -400
  378. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  379. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  380. package/dist/packages/data/src/trade/swap-build.js +0 -108
  381. package/dist/packages/data/src/trade/token-delta.js +0 -201
  382. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  383. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  384. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  385. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  386. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  387. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  388. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  389. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  390. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  391. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  392. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  393. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  394. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  395. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,776 @@
1
+ // dexterm — AUTOTRADE (paper) screen renderer (`Q`, task-1508, AUTOTRADE-SPEC §2b): strategies, editor, compare,
2
+ // backtest and runner views over AutotradeUi (autotrade-ui.ts). Read-only drawing; the verbs live in
3
+ // autotrade-keys.ts. Every view says PAPER; no view offers live mode, a wallet, a key or the signer.
4
+ import { Canvas, header, footer, breakpoint, fmtAgeSec, sparkline, wrap } from '../../../tui/src/index.js';
5
+ import { compareRows as fleetCompareRows, compareTable as fleetCompareTable, fleetTotals } from '../../../autotrade/src/index.js';
6
+ import { tokenize } from "../screener/filter.js";
7
+ import { headerModel } from "../state.js";
8
+ import { journeyLines, refreshJourney } from "./autotrade-journey.js";
9
+ import { AUTOTRADE_VIEWS, VIEW_TITLE, READ_ONLY_PATHS, backtestSubject, compareRows, getAt, heartbeatStale, issuesByField, screenHighlight, } from "./autotrade-ui.js";
10
+ /** Per-screen memo of file reads + validation, keyed by what the list says about a strategy (id, hash, version,
11
+ * last backtest): a frame never re-reads a strategy file; a save, import or backtest reloads the list and so the key. */
12
+ const MEMO = new WeakMap();
13
+ function memo(ui, key, f) {
14
+ let m = MEMO.get(ui);
15
+ if (!m) {
16
+ m = new Map();
17
+ MEMO.set(ui, m);
18
+ }
19
+ if (!m.has(key)) {
20
+ if (m.size > 32)
21
+ m.clear();
22
+ m.set(key, f());
23
+ }
24
+ return m.get(key);
25
+ }
26
+ const entryKey = (ui, id) => { const e = ui.entries.find((x) => x.id === id); return e ? `${e.id}|${e.source}|${e.hash ?? ''}|${e.version ?? ''}|${e.lastBacktest?.t ?? ''}|${e.ok}` : `${id}|missing`; };
27
+ function fit(s, w) { return w <= 0 ? '' : s.length > w ? (w > 1 ? s.slice(0, w - 1) + '…' : s.slice(0, w)) : s; }
28
+ const usd = (x) => `${x >= 0 ? '+' : '-'}$${Math.abs(x).toFixed(2)}`;
29
+ const pct = (x, dp = 1) => (x === null || x === undefined ? '—' : `${x >= 0 ? '+' : ''}${x.toFixed(dp)}%`);
30
+ export function renderAutotrade(c, state) {
31
+ const t = c.theme;
32
+ header(c, headerModel(state));
33
+ const ui = state.autotradeUi ?? null;
34
+ if (!ui) {
35
+ c.text(1, 3, 'autotrade is not loaded', { fg: t.dim, bg: t.bg });
36
+ return;
37
+ }
38
+ const now = ui.deps.now();
39
+ tabs(c, ui);
40
+ const top = 3, bottom = c.h - 1;
41
+ const area = { x: 0, y: top, w: c.w, h: bottom - top };
42
+ if (ui.view === 'list')
43
+ renderList(c, ui, area, now);
44
+ else if (ui.view === 'editor')
45
+ renderEditor(c, ui, area);
46
+ else if (ui.view === 'compare')
47
+ renderCompare(c, ui, area);
48
+ else if (ui.view === 'backtest')
49
+ renderBacktest(c, ui, area, now);
50
+ else if (ui.view === 'journey')
51
+ renderJourneyView(c, ui, area, now);
52
+ else
53
+ renderRunner(c, ui, area, now);
54
+ if (ui.confirm?.kind === 'flatten')
55
+ confirmCard(c);
56
+ else if (ui.confirm?.kind === 'promote')
57
+ promoteCard(c, ui, ui.confirm.proposalId);
58
+ if (ui.review)
59
+ reviewCard(c, ui);
60
+ footer(c, hintsFor(ui), t.name);
61
+ }
62
+ /** Row 2: PAPER badge + the five views (the active one lit) + the notice / input line on the right. */
63
+ function tabs(c, ui) {
64
+ const t = c.theme;
65
+ const y = 2;
66
+ c.fill(0, y, c.w, 1, ' ', { bg: t.bg });
67
+ // task-1528: a live run (through the live signer) is labelled LIVE (mainnet, red) or DEVNET; everything else PAPER.
68
+ const mode = runMode(ui);
69
+ let x = c.text(1, y, mode === 'live' ? ' LIVE · REAL FUNDS ' : mode === 'devnet' ? ' DEVNET ' : ' PAPER ', { fg: t.bg, bg: mode === 'live' ? t.down : t.warn, b: true });
70
+ x = c.text(x + 1, y, 'AUTOTRADE', { fg: t.accent, bg: t.bg, b: true }) + 2;
71
+ for (const v of AUTOTRADE_VIEWS) {
72
+ const on = ui.view === v;
73
+ const off = (v === 'compare' && !ui.compare) || (v === 'editor' && !ui.editor);
74
+ x = c.text(x, y, ` ${VIEW_TITLE[v]} `, { fg: on ? t.text : off ? t.dim : t.muted, bg: on ? t.tabActiveBg : t.bg, b: on }) + 1;
75
+ }
76
+ if (ui.view === 'journey')
77
+ x = c.text(x, y, ` ${VIEW_TITLE.journey} `, { fg: t.text, bg: t.tabActiveBg, b: true }) + 1;
78
+ const line = ui.input
79
+ ? `${ui.input.kind === 'import' ? 'import file: ' : ui.input.kind === 'dup' ? `duplicate ${ui.input.from} as: ` : `new from ${ui.input.from} as: `}${ui.input.text}█ ⏎ ok · esc cancel`
80
+ : ui.notice;
81
+ if (line && x < c.w - 10)
82
+ c.text(x + 1, y, fit(line, c.w - x - 2), { fg: ui.input ? t.text : t.warn, bg: t.bg, i: !ui.input });
83
+ }
84
+ function hintsFor(ui) {
85
+ const common = [['esc', 'back'], ['q', 'quit']];
86
+ if (ui.confirm)
87
+ return [['y', ui.confirm.kind === 'promote' ? 'apply promotion' : 'flatten'], ['any key', 'cancel']];
88
+ if (ui.review)
89
+ return ui.review.stage === 'less-safe' ? [['y', 'apply (less safe)'], ['any key', 'cancel']] : [['y', ui.review.check.ok && ui.review.check.riskDelta === 'looser' ? 'apply… (less safe)' : 'apply'], ['n', 'dismiss'], ['any key', 'close']];
90
+ if (ui.input)
91
+ return [['⏎', 'ok'], ['esc', 'cancel']];
92
+ switch (ui.view) {
93
+ case 'list': return [['tab', 'view'], ['⏎/e', 'edit'], ['n', 'new'], ['d', 'copy'], ['c', 'compare'], ['b', 'backtest'], ['w', 'sweep'], ['x/i', 'export/import'], ['o', 'chat proposal'], ['r', 'runner'], ...common];
94
+ case 'editor': return ui.editor?.typing
95
+ ? [['⏎', 'set'], ['esc', 'cancel'], ['ctrl+u', 'clear']]
96
+ : [['tab', 'view'], ['↑↓', 'field'], ['⏎', 'change'], ['s', 'save'], ['u', 'undo'], ['b', 'backtest'], ['w', 'sweep'], ...common];
97
+ case 'compare': return [['tab', 'view'], ['↑↓', 'scroll'], ['b', 'backtest both, one tape'], ...common];
98
+ case 'backtest': return [['tab', 'view'], ['b', 'run again'], ['w', 'sweep (held-out)'], ...common];
99
+ case 'journey': return [['b', 'backtest'], ['F', 'forward test'], ['X', 'stop it'], ['A', 'promotion'], ['J', 'refresh'], ...common];
100
+ default: return [['tab', 'view'], ['!', 'kill'], ['p', 'pause entries'], ['f', 'flatten (confirm)'], ...common];
101
+ }
102
+ }
103
+ // ───────────────────────────── strategies ─────────────────────────────
104
+ function renderList(c, ui, a, now) {
105
+ const t = c.theme;
106
+ const bp = breakpoint(c.w);
107
+ const users = ui.entries.filter((e) => e.source === 'user').length;
108
+ // the list takes the rows it needs (min 8); the selected strategy and the chat proposals get the rest
109
+ const listH = Math.min(a.h - 10, Math.max(ui.entries.length + 3, a.h - 16));
110
+ const lowerH = a.h - listH;
111
+ const box = c.box(a.x, a.y, a.w, listH, {
112
+ title: `STRATEGIES · ${ui.entries.length} (${ui.entries.length - users} presets, ${users} yours)`,
113
+ caption: bp === 'compact' ? undefined : 'paper only · your files: ~/.dexterm/strategies · presets are read-only (d makes a copy)', focus: !ui.input,
114
+ });
115
+ const bg = box.bg;
116
+ let y = box.y;
117
+ const active = ui.runner.kind === 'ok' ? ui.runner.snap.strategy : null;
118
+ const wide = bp !== 'compact';
119
+ const cols = wide
120
+ ? { mark: 1, id: 4, src: 26, mode: 34, ver: 41, hash: 50, prof: 68, last: Math.max(82, box.w - 44) }
121
+ : { mark: 1, id: 4, src: 24, mode: 31, ver: 37, hash: -1, prof: -1, last: 45 };
122
+ const head = [[cols.id, 'STRATEGY'], [cols.src, 'SOURCE'], [cols.mode, 'MODE'], [cols.ver, 'VERSION'], [cols.hash, 'HASH'], [cols.prof, 'PROFILES'], [cols.last, 'LAST BACKTEST · net · dd · n']];
123
+ for (const [x, h] of head)
124
+ if (x >= 0)
125
+ c.text(box.x + x, y, h, { fg: t.dim, bg });
126
+ y += 1;
127
+ if (!ui.entries.length)
128
+ c.text(box.x + 1, y + 1, 'no strategies found (the shipped presets should always list: reinstall if this stays empty)', { fg: t.warn, bg });
129
+ const rows = box.h - 1;
130
+ const scroll = Math.max(0, ui.selected - (rows - 1));
131
+ for (let i = scroll; i < ui.entries.length && y < box.y + box.h; i++) {
132
+ const e = ui.entries[i];
133
+ const sel = i === ui.selected;
134
+ const rb = sel ? t.selBg : bg;
135
+ c.fill(box.x, y, box.w, 1, ' ', { bg: rb });
136
+ if (sel)
137
+ c.put(box.x, y, '▎', { fg: t.accent, bg: rb, soft: true });
138
+ const running = active && active.id === e.id;
139
+ const marked = ui.marked.includes(e.id);
140
+ c.text(box.x + cols.mark, y, running ? '▶' : marked ? '◆' : ' ', { fg: running ? t.up : t.accent, bg: rb, b: true });
141
+ c.text(box.x + cols.id, y, fit(e.highRisk ? `${e.id} ⚠` : e.id, cols.src - cols.id - 1), { fg: e.ok ? (e.highRisk ? t.warn : sel ? t.borderFocus : t.text) : t.down, bg: rb, b: sel });
142
+ c.text(box.x + cols.src, y, e.source === 'preset' ? 'preset' : e.shadowsPreset ? 'yours*' : 'yours', { fg: e.source === 'preset' ? t.muted : t.accent, bg: rb });
143
+ if (!e.ok) {
144
+ c.text(box.x + cols.mode, y, fit(`✗ invalid: ${e.errors?.[0] ? `${e.errors[0].path || '/'} ${e.errors[0].msg}` : ''}`, box.w - cols.mode - 2), { fg: t.down, bg: rb });
145
+ y += 1;
146
+ continue;
147
+ }
148
+ c.text(box.x + cols.mode, y, e.mode ?? '—', { fg: e.mode === 'paper' ? t.up : t.warn, bg: rb });
149
+ c.text(box.x + cols.ver, y, `v${e.version}`, { fg: t.muted, bg: rb });
150
+ if (cols.hash >= 0)
151
+ c.text(box.x + cols.hash, y, e.hash ?? '', { fg: t.dim, bg: rb });
152
+ if (cols.prof >= 0)
153
+ c.text(box.x + cols.prof, y, fit((e.profiles ?? []).join(' '), cols.last - cols.prof - 1), { fg: t.muted, bg: rb });
154
+ const lb = e.lastBacktest;
155
+ const job = e.hash ? ui.backtests.get(e.hash) : undefined;
156
+ const last = job?.status === 'running' ? 'running…' : lb ? `${usd(lb.netUsd)} (${pct(lb.netPct)}) · ${lb.maxDrawdownPct.toFixed(1)}% · ${lb.trades}` : 'not backtested';
157
+ c.text(box.x + cols.last, y, fit(last, box.w - cols.last - 2), { fg: lb ? (lb.netUsd >= 0 ? t.up : t.down) : t.dim, bg: rb });
158
+ y += 1;
159
+ }
160
+ // lower pane: the selected strategy + chat proposals
161
+ const ly0 = a.y + listH;
162
+ const sel = ui.entries[ui.selected];
163
+ const props = ui.deps.proposals();
164
+ const half = wide ? Math.floor(a.w * 0.58) : a.w;
165
+ const dbox = c.box(a.x, ly0, half, lowerH, { title: sel ? `${sel.id}${sel.name ? ` · ${sel.name}` : ''}` : 'no strategy', caption: running(ui) ? undefined : 'b backtests on recorded history · r shows the runner' });
166
+ let dy = dbox.y;
167
+ const g = sel ? memo(ui, `get:${entryKey(ui, sel.id)}`, () => ui.deps.store.get(sel.id)) : null;
168
+ if (g?.ok) {
169
+ const s = g.strategy;
170
+ const line = (k, v, fg = t.text) => {
171
+ wrap(v, dbox.w - 13).forEach((l, i) => { if (dy >= dbox.y + dbox.h)
172
+ return; c.text(dbox.x + 1, dy, i ? '' : k.padEnd(10), { fg: t.dim, bg: dbox.bg }); c.text(dbox.x + 11, dy, fit(l, dbox.w - 13), { fg, bg: dbox.bg }); dy += 1; });
173
+ };
174
+ if (s.highRisk)
175
+ line('⚠ HIGH', 'HIGH RISK: full port live (the signer sizes an entry up to the caps you set, up to the whole wallet); one rug can zero it. Experimental, not financial advice.', t.down);
176
+ line('screen', s.entry.screen, t.accent);
177
+ line('age', `${s.entry.ageHours.min}–${s.entry.ageHours.max ?? '∞'} h · ${s.entry.triggers.map((x) => x.kind).join(` ${s.entry.triggerMode} `)} · decide ${s.entry.decide}`);
178
+ line('sizing', `${s.sizing.mode}${s.sizing.pct !== undefined ? ` ${s.sizing.pct}%` : s.sizing.riskPct !== undefined ? ` risk ${s.sizing.riskPct}%` : s.sizing.fixedUsd !== undefined ? ` $${s.sizing.fixedUsd}` : ''} · cap ${s.sizing.maxPctOfBankroll}% / $${s.sizing.maxUsd} · book $${s.bankroll.startUsd}`);
179
+ line('risk', `${s.risk.maxConcurrent} open · ${s.risk.maxExposurePct}% exposure · day halt −${s.risk.dailyLossHaltPct}% · dd ½ at ${s.risk.drawdown.sizeDownAtPct}%, halt ${s.risk.drawdown.haltAtPct}% · ${s.risk.maxEntriesPerDay}/day`);
180
+ for (const p of Object.values(s.exits.profiles))
181
+ line(p.id === s.exits.default ? 'exits ●' : 'exits', ladderSketch(p), t.muted);
182
+ for (const w of g.warnings.slice(0, 2))
183
+ line('warning', `${w.path} ${w.msg}`, t.warn);
184
+ }
185
+ else if (g && !g.ok) {
186
+ for (const e of g.errors.slice(0, dbox.h)) {
187
+ c.text(dbox.x + 1, dy, fit(`✗ ${e.path || '/'} ${e.msg}`, dbox.w - 2), { fg: t.down, bg: dbox.bg });
188
+ dy += 1;
189
+ }
190
+ }
191
+ if (wide) {
192
+ const pbox = c.box(a.x + half, ly0, a.w - half, lowerH, { title: `PROPOSALS FROM CHAT · ${props.length}`, caption: 'o reviews · applied only by your keypress' });
193
+ if (!props.length) {
194
+ let py = pbox.y;
195
+ for (const [txt, it] of [['none yet · ask the analyst to change a strategy and its diff card shows up here', true], ['', false], ['a model only ever proposes: it cannot switch a strategy to live, raise its own spend or apply anything', false]]) {
196
+ for (const l of txt ? wrap(txt, pbox.w - 2) : ['']) {
197
+ if (py >= pbox.y + pbox.h)
198
+ break;
199
+ c.text(pbox.x + 1, py, l, { fg: t.dim, bg: pbox.bg, i: it });
200
+ py += 1;
201
+ }
202
+ }
203
+ }
204
+ else {
205
+ let py = pbox.y;
206
+ for (const p of props) {
207
+ py = proposalCard(c, p, pbox.x + 1, py, pbox.w - 2, pbox.y + pbox.h);
208
+ if (py >= pbox.y + pbox.h)
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ void now;
214
+ }
215
+ const running = (ui) => ui.runner.kind === 'ok';
216
+ /** task-1528: the running runner's mode from its rendezvous ('live' = mainnet through the live signer). */
217
+ const runMode = (ui) => { const m = ui.runner.kind === 'ok' ? ui.runner.snap.mode : 'paper'; return m === 'live' || m === 'devnet' || m === 'real' ? m : 'paper'; };
218
+ /** One chat proposal card (§2a): id, hashes, the diff lines, validation, LOOSER in the warn colour. */
219
+ export function proposalCard(c, p, x, y, w, maxY) {
220
+ const t = c.theme;
221
+ const bg = c.theme.panel;
222
+ const looser = p.riskDelta === 'looser';
223
+ if (y < maxY) {
224
+ c.text(x, y, fit(`${p.id} ${p.beforeHash} → ${p.afterHash}`, w - 10), { fg: t.text, bg, b: true });
225
+ c.textR(x, y, w, looser ? 'LOOSER' : p.riskDelta.toUpperCase(), { fg: looser ? t.warn : p.riskDelta === 'tighter' ? t.up : t.muted, bg, b: true });
226
+ y += 1;
227
+ }
228
+ for (const d of p.diff.slice(0, 4)) {
229
+ if (y >= maxY)
230
+ return y;
231
+ c.text(x + 1, y, fit(d, w - 1), { fg: d.startsWith('+') ? t.up : d.startsWith('-') ? t.down : t.muted, bg });
232
+ y += 1;
233
+ }
234
+ if (y < maxY) {
235
+ c.text(x + 1, y, fit(p.validation.ok ? `✓ valid · ${p.why}` : `✗ ${p.validation.errors[0]?.path ?? ''} ${p.validation.errors[0]?.msg ?? ''}`, w - 1), { fg: p.validation.ok ? t.dim : t.down, bg, i: true });
236
+ y += 1;
237
+ }
238
+ return y + 1;
239
+ }
240
+ /**
241
+ * task-1507: the review card of one chat proposal (`o`). The full diff, who proposed it and why, the validation, and
242
+ * every less-safe field when it is LOOSER. On a looser edit the first y turns the card into the "less safe" popup and
243
+ * only a second y applies it.
244
+ */
245
+ function reviewCard(c, ui) {
246
+ const t = c.theme;
247
+ const rv = ui.review;
248
+ const r = rv.rec;
249
+ const chk = rv.check;
250
+ const looser = chk.ok && chk.riskDelta === 'looser';
251
+ const lessSafe = rv.stage === 'less-safe';
252
+ const w = Math.min(96, c.w - 4), h = Math.min(c.h - 4, 24);
253
+ const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
254
+ c.noRain(x, y, w, h);
255
+ const delta = chk.ok ? chk.riskDelta.toUpperCase() : 'CANNOT APPLY';
256
+ const b = c.box(x, y, w, h, { title: `${lessSafe ? '⚠ LESS SAFE · ' : ''}CHAT PROPOSAL · ${r.kind === 'new' ? `new ${r.id} from ${r.from}` : r.id} · ${delta} · PAPER`, focus: true, bg: t.panelAlt, border: looser || !chk.ok ? t.warn : t.accent });
257
+ const bg = b.bg;
258
+ let yy = b.y;
259
+ const line = (s, st) => { if (yy < b.y + b.h) {
260
+ c.text(b.x + 1, yy, fit(s, b.w - 2), { ...st, bg });
261
+ yy += 1;
262
+ } };
263
+ line(`proposed by ${r.actor} · ${r.proposalId} · ${r.beforeHash} → ${r.afterHash} · from v${r.baseVersion}`, { fg: t.muted });
264
+ for (const l of wrap(`why: ${r.why}`, b.w - 2).slice(0, 2))
265
+ line(l, { fg: t.text, i: true });
266
+ if (!chk.ok) {
267
+ yy += 1;
268
+ for (const e of chk.errors.slice(0, 6))
269
+ line(`✗ ${e.path || '/'} ${e.msg}`, { fg: t.down });
270
+ line('n dismisses it · any other key closes', { fg: t.warn, b: true });
271
+ return;
272
+ }
273
+ const less = chk.reasons.filter((x) => x.dir === 'looser');
274
+ if (lessSafe) {
275
+ yy += 1;
276
+ line('THIS IS LESS SAFE. These risk numbers get looser:', { fg: t.warn, b: true });
277
+ for (const x of less.slice(0, Math.max(1, b.h - 7)))
278
+ line(` ${x.path} ${x.from} → ${x.to}${x.note ? ` (${x.note})` : ''}`, { fg: t.warn });
279
+ yy = Math.max(yy, b.y + b.h - 2);
280
+ line('y applies it anyway (a new version, journaled) · any other key cancels', { fg: t.warn, b: true });
281
+ return;
282
+ }
283
+ line(`✓ valid${chk.validation.warnings.length ? ` · ${chk.validation.warnings.length} warning(s): ${chk.validation.warnings[0].path} ${chk.validation.warnings[0].msg}` : ''}`, { fg: t.up });
284
+ if (looser)
285
+ line(`LOOSER: ${less.map((x) => x.path.split('/').slice(-2).join('/')).join(', ')}`, { fg: t.warn, b: true });
286
+ const room = b.y + b.h - 1 - yy; // the last row is the key line
287
+ const diff = chk.diff.length > room ? [...chk.diff.slice(0, Math.max(0, room - 1)), `… ${chk.diff.length - room + 1} more lines`] : chk.diff;
288
+ for (const d of diff)
289
+ line(d, { fg: d.startsWith('+') ? t.up : d.startsWith('-') ? t.down : t.muted });
290
+ c.text(b.x + 1, b.y + b.h - 1, fit(looser ? 'y opens the less-safe popup (a second y applies) · n dismisses · any other key closes' : 'y applies it (a new version, journaled) · n dismisses · any other key closes', b.w - 2), { fg: looser ? t.warn : t.accent, bg, b: true });
291
+ }
292
+ /** A one-line sketch of an exit profile: stop · cost-back · TP steps · trails · time/hold limits. */
293
+ export function ladderSketch(p) {
294
+ const parts = [];
295
+ parts.push(p.hardStop.enabled && p.hardStop.atPct !== null ? `stop ${p.hardStop.atPct}% ┤` : 'no stop ┤');
296
+ parts.push('entry');
297
+ const steps = p.tpLadder.map((s) => [s.atPct, `+${s.atPct}% ${frac(s.sellFrac)}`]);
298
+ if (p.costRecovery)
299
+ steps.push([p.costRecovery.atPct, `+${p.costRecovery.atPct}% cost back (≤${Math.round(p.costRecovery.maxSellFrac * 100)}%)`]);
300
+ steps.sort((x, y) => x[0] - y[0]);
301
+ for (const [, s] of steps)
302
+ parts.push(s);
303
+ const tail = [];
304
+ if (p.trailingStop)
305
+ tail.push(`trail ${p.trailingStop.trailPct}% after +${p.trailingStop.armAtPct}%`);
306
+ if (p.bagTrail)
307
+ tail.push(`bag trail ${p.bagTrail.trailPct}%${p.bagTrail.armAtPct ? ` after +${p.bagTrail.armAtPct}%` : ''}`);
308
+ if (p.timeStop)
309
+ tail.push(`${p.timeStop.minutes}m ${p.timeStop.action}`);
310
+ if (p.maxHoldMinutes)
311
+ tail.push(`hold ≤ ${p.maxHoldMinutes >= 1440 ? `${+(p.maxHoldMinutes / 1440).toFixed(1)}d` : p.maxHoldMinutes >= 60 ? `${+(p.maxHoldMinutes / 60).toFixed(1)}h` : `${p.maxHoldMinutes}m`}`);
312
+ return `${p.id}: ${parts.join(' ── ')}${tail.length ? ` · ${tail.join(' · ')}` : ''}`;
313
+ }
314
+ const frac = (f) => ({ 0.5: '½', 0.25: '¼', 0.75: '¾' }[f] ?? (Math.abs(f - 1 / 3) < 0.01 ? '⅓' : `${Math.round(f * 100)}%`));
315
+ // ───────────────────────────── editor ─────────────────────────────
316
+ function renderEditor(c, ui, a) {
317
+ const t = c.theme;
318
+ const ed = ui.editor;
319
+ if (!ed) {
320
+ c.text(a.x + 1, a.y + 1, 'no strategy open · e on the list opens one', { fg: t.dim, bg: t.bg });
321
+ return;
322
+ }
323
+ const wide = breakpoint(c.w) !== 'compact';
324
+ const formW = wide ? Math.floor(a.w * 0.6) : a.w;
325
+ const inputH = ed.typing ? 4 : 0;
326
+ const check = ed.typing?.check ?? ed.check;
327
+ const unsaved = ed.edits.length;
328
+ const status = ed.typing ? (ed.typing.parseError ? '✗ not a value' : check.ok ? '✓ valid' : `✗ ${check.errors.length} problem${check.errors.length === 1 ? '' : 's'}`) : check.ok ? '✓ valid' : `✗ ${check.errors.length} problem${check.errors.length === 1 ? '' : 's'}`;
329
+ const box = c.box(a.x, a.y, formW, a.h - inputH, {
330
+ title: `${ed.id} · v${ed.baseVersion}${unsaved ? ` + ${unsaved} unsaved` : ''} · ${ed.draft.mode.toUpperCase()} · ${status}`,
331
+ caption: ed.source === 'preset' ? 'shipped preset: read-only (d on the list makes your copy)' : 's saves a new version (only when valid) · u undoes', focus: !ed.typing,
332
+ });
333
+ const bg = box.bg;
334
+ const { at } = issuesByField(ed.fields, ed.check.ok ? [] : ed.check.errors);
335
+ const typingIssues = ed.typing?.check && !ed.typing.check.ok ? ed.typing.check.errors : [];
336
+ const rows = [];
337
+ let last = '';
338
+ ed.fields.forEach((f, i) => { if (f.section !== last) {
339
+ rows.push({ kind: 'section', s: f.section });
340
+ last = f.section;
341
+ } rows.push({ kind: 'field', i }); });
342
+ const selRow = rows.findIndex((r) => r.kind === 'field' && r.i === ed.selected);
343
+ const view = box.h;
344
+ const scroll = Math.max(0, Math.min(rows.length - view, selRow - Math.floor(view / 2)));
345
+ const labelW = Math.min(36, Math.max(...ed.fields.map((f) => f.path.length - f.section.length)) + 2, Math.floor(box.w * 0.45));
346
+ let y = box.y;
347
+ for (let r = scroll; r < rows.length && y < box.y + box.h; r++, y++) {
348
+ const row = rows[r];
349
+ if (row.kind === 'section') {
350
+ c.text(box.x + 1, y, row.s.toUpperCase(), { fg: t.accent, bg, b: true });
351
+ c.hline(box.x + 2 + row.s.length, y, box.w - row.s.length - 4, { fg: t.border, bg, soft: true });
352
+ continue;
353
+ }
354
+ const f = ed.fields[row.i];
355
+ const sel = row.i === ed.selected;
356
+ const rb = sel ? t.selBg : bg;
357
+ c.fill(box.x, y, box.w, 1, ' ', { bg: rb });
358
+ if (sel)
359
+ c.put(box.x, y, '▎', { fg: t.accent, bg: rb, soft: true });
360
+ const changed = ed.edits.some((x) => x.op.path === f.path);
361
+ c.text(box.x + 2, y, fit(f.path.slice(f.section.length + 2) || f.path, labelW - 2), { fg: READ_ONLY_PATHS.has(f.path) ? t.dim : changed ? t.warn : t.muted, bg: rb });
362
+ const vx = box.x + labelW + 1;
363
+ const vw = box.w - labelW - 3;
364
+ const v = getAt(ed.draft, f.path);
365
+ const errs = at.get(f.path) ?? [];
366
+ const text = f.kind === 'string' ? String(v) : JSON.stringify(v);
367
+ // the value gets what it needs; an error takes the rest of the row
368
+ const valW = errs.length ? Math.min(text.length + 1, Math.floor(vw * 0.5)) : vw;
369
+ if (f.path === '/entry/screen')
370
+ screenValue(c, String(v ?? ''), vx, y, valW, rb);
371
+ else
372
+ c.text(vx, y, fit(text, valW), { fg: changed ? t.warn : t.text, bg: rb, b: changed });
373
+ if (errs.length) {
374
+ const mx = vx + valW + 1;
375
+ c.text(mx, y, fit(`✗ ${errs[0].msg}`, box.x + box.w - mx - 1), { fg: t.down, bg: rb });
376
+ }
377
+ }
378
+ if (ed.typing) {
379
+ const iy = a.y + a.h - inputH;
380
+ const ib = c.box(a.x, iy, formW, inputH, { title: `set ${ed.typing.path}`, caption: '⏎ set · esc cancel · re-validated on every key', focus: true });
381
+ const shownW = ib.w - 4;
382
+ const txt = ed.typing.text;
383
+ const shown = txt.length > shownW ? '…' + txt.slice(txt.length - shownW + 1) : txt;
384
+ if (ed.typing.path === '/entry/screen')
385
+ screenValue(c, shown, ib.x + 1, ib.y, shownW, ib.bg);
386
+ else
387
+ c.text(ib.x + 1, ib.y, shown, { fg: t.text, bg: ib.bg });
388
+ c.put(ib.x + 1 + Math.min(shown.length, shownW), ib.y, '█', { fg: t.accent, bg: ib.bg, soft: true });
389
+ // the live result under the value: for the screen filter a caret at the parser's error column
390
+ const ty = ed.typing;
391
+ const own = ty.check && !ty.check.ok ? ty.check.errors.find((e) => e.path === ty.path || e.path.startsWith(`${ty.path}/`)) : undefined;
392
+ const sh = ty.path === '/entry/screen' ? screenHighlight(txt) : null;
393
+ const res = ty.parseError ? `✗ ${ty.parseError}` : own ? `✗ ${own.msg}` : ty.check && !ty.check.ok ? `✓ this field · ✗ ${ty.check.errors.length} elsewhere` : '✓ valid';
394
+ const shift = txt.length - shown.length;
395
+ if (sh?.at && sh.at - 1 - shift >= 0 && sh.at - 1 - shift < shownW) {
396
+ const cx = ib.x + 1 + sh.at - 1 - shift;
397
+ c.put(cx, ib.y + 1, '^', { fg: t.down, bg: ib.bg, b: true });
398
+ c.text(cx + 2, ib.y + 1, fit(res, ib.x + ib.w - cx - 4), { fg: t.down, bg: ib.bg });
399
+ }
400
+ else
401
+ c.text(ib.x + 1, ib.y + 1, fit(res, ib.w - 2), { fg: res.startsWith('✓') ? t.up : t.down, bg: ib.bg });
402
+ }
403
+ if (!wide)
404
+ return;
405
+ // right column: validation of the draft (or of the value being typed) + the exit ladders
406
+ const rx = a.x + formW, rw = a.w - formW;
407
+ const vh = Math.max(6, Math.floor(a.h * 0.45));
408
+ const vbox = c.box(rx, a.y, rw, vh, { title: ed.typing ? 'VALIDATION · with the value you are typing' : 'VALIDATION', caption: 'dexterm-strategy/1 + cross-field rules' });
409
+ let vy = vbox.y;
410
+ const vline = (s, fg) => { wrap(s, vbox.w - 4).forEach((l, i) => { if (vy < vbox.y + vbox.h) {
411
+ c.text(vbox.x + 1 + (i ? 2 : 0), vy, fit(l, vbox.w - 2 - (i ? 2 : 0)), { fg, bg: vbox.bg });
412
+ vy += 1;
413
+ } }); };
414
+ if (ed.typing?.parseError)
415
+ vline(`✗ ${ed.typing.parseError}`, t.down);
416
+ const shownIssues = ed.typing ? typingIssues : ed.check.ok ? [] : ed.check.errors;
417
+ if (!ed.typing?.parseError && !shownIssues.length)
418
+ vline('✓ valid: every field and the cross-field rules pass', t.up);
419
+ for (const i of shownIssues)
420
+ vline(`✗ ${i.path || '/'} ${i.msg}`, t.down);
421
+ for (const w of (ed.typing?.check ?? ed.check).warnings)
422
+ vline(`! ${w.path} ${w.msg}`, t.warn);
423
+ const lbox = c.box(rx, a.y + vh, rw, a.h - vh, { title: 'EXIT PROFILES', caption: 'stop ┤ entry ── take-profits ── trails · limits' });
424
+ let ly = lbox.y;
425
+ const chk = ed.check;
426
+ const profiles = chk.ok ? Object.values(chk.strategy.exits.profiles) : [];
427
+ if (!profiles.length)
428
+ c.text(lbox.x + 1, ly, 'fix the problems to see the ladders', { fg: t.dim, bg: lbox.bg, i: true });
429
+ for (const p of profiles) {
430
+ if (ly >= lbox.y + lbox.h - 1)
431
+ break;
432
+ c.text(lbox.x + 1, ly, fit(`${p.id === ed.draft.exits.default ? '● ' : ' '}${p.name}`, lbox.w - 2), { fg: t.accent, bg: lbox.bg, b: true });
433
+ ly += 1;
434
+ for (const l of wrapSketch(ladderSketch(p).replace(/^[^:]+: /, ''), lbox.w - 4).slice(0, 2)) {
435
+ c.text(lbox.x + 3, ly, l, { fg: t.muted, bg: lbox.bg });
436
+ ly += 1;
437
+ }
438
+ }
439
+ }
440
+ function wrapSketch(s, w) {
441
+ const out = [];
442
+ let cur = '';
443
+ for (const part of s.split(' · ')) {
444
+ const next = cur ? `${cur} · ${part}` : part;
445
+ if (next.length > w && cur) {
446
+ out.push(cur);
447
+ cur = part;
448
+ }
449
+ else
450
+ cur = next;
451
+ }
452
+ if (cur)
453
+ out.push(fit(cur, w));
454
+ return out;
455
+ }
456
+ /** The screen filter in the screener's own tokens: fields / ops / tags coloured, the parser's error column lit. */
457
+ function screenValue(c, src, x, y, w, bg) {
458
+ const t = c.theme;
459
+ const shown = fit(src, w);
460
+ c.text(x, y, shown, { fg: t.text, bg });
461
+ for (const tok of tokenize(shown)) {
462
+ const fg = tok.kind === 'op' ? t.warn : tok.kind === 'not' ? t.down : tok.kind === 'colon' ? t.dim : /^[0-9.]/.test(tok.text) ? t.up : t.accent;
463
+ c.text(x + tok.at - 1, y, tok.text, { fg, bg });
464
+ }
465
+ const h = screenHighlight(src);
466
+ if (h.at !== null && h.at - 1 < shown.length)
467
+ c.put(x + Math.max(0, h.at - 1), y, shown[Math.max(0, h.at - 1)] ?? ' ', { fg: t.bg, bg: t.down, b: true });
468
+ }
469
+ // ───────────────────────────── compare ─────────────────────────────
470
+ function renderCompare(c, ui, a) {
471
+ const t = c.theme;
472
+ const cmp = ui.compare;
473
+ if (!cmp) {
474
+ c.text(a.x + 1, a.y + 1, 'c on two strategies in the list compares them', { fg: t.dim, bg: t.bg });
475
+ return;
476
+ }
477
+ const store = ui.deps.store;
478
+ const { rows, lastA, lastB } = memo(ui, `cmp:${entryKey(ui, cmp.a)}:${entryKey(ui, cmp.b)}`, () => {
479
+ const ga = store.get(cmp.a), gb = store.get(cmp.b);
480
+ const bts = store.backtests();
481
+ return {
482
+ rows: compareRows(store.raw(cmp.a), store.raw(cmp.b)),
483
+ lastA: ga?.ok ? bts[ga.strategy.hash] : undefined, lastB: gb?.ok ? bts[gb.strategy.hash] : undefined,
484
+ };
485
+ });
486
+ const changed = rows.filter((r) => r.changed).length;
487
+ const btH = 6;
488
+ const bt = c.box(a.x, a.y, a.w, btH, { title: 'BACKTESTS · same tape', caption: 'b runs both on one window of recorded history' });
489
+ const half = Math.floor((bt.w - 2) / 2);
490
+ const job = ui.compareRun;
491
+ const cell = (i, id, last, x) => {
492
+ let y = bt.y;
493
+ c.text(x, y, fit(id, half - 1), { fg: t.accent, bg: bt.bg, b: true });
494
+ y += 1;
495
+ const out = job?.status === 'done' ? job.result[i] : null;
496
+ if (out) {
497
+ for (const l of reportLines(out.report).slice(0, 3)) {
498
+ c.text(x, y, fit(l, half - 1), { fg: out.report.netUsd >= 0 ? t.up : t.down, bg: bt.bg });
499
+ y += 1;
500
+ }
501
+ return;
502
+ }
503
+ const line = job?.status === 'running' ? 'running…' : job?.status === 'error' ? `✗ ${job.error}` : last ? `last: ${usd(last.netUsd)} · dd ${last.maxDrawdownPct.toFixed(1)}% · ${last.trades} trades · ${last.tape}` : 'not backtested yet';
504
+ c.text(x, y, fit(line, half - 1), { fg: job?.status === 'error' ? t.down : t.muted, bg: bt.bg });
505
+ };
506
+ cell(0, cmp.a, lastA, bt.x + 1);
507
+ cell(1, cmp.b, lastB, bt.x + 1 + half);
508
+ if (job?.status === 'done')
509
+ c.text(bt.x + 1, bt.y + bt.h - 1, fit(`tape: ${job.result[0].report.tape.mints} coins · ${job.result[0].report.tape.from ?? '?'} → ${job.result[0].report.tape.to ?? '?'} (both)`, bt.w - 2), { fg: t.dim, bg: bt.bg });
510
+ const box = c.box(a.x, a.y + btH, a.w, a.h - btH, { title: `FIELDS · ${changed} differ of ${rows.length}`, caption: 'changed fields lit · builtin profiles compare as references', focus: true });
511
+ const pw = Math.min(46, Math.floor(box.w * 0.34));
512
+ const vw = Math.floor((box.w - pw - 3) / 2);
513
+ c.text(box.x + 1, box.y, 'FIELD', { fg: t.dim, bg: box.bg });
514
+ c.text(box.x + 1 + pw, box.y, fit(cmp.a, vw), { fg: t.dim, bg: box.bg });
515
+ c.text(box.x + 2 + pw + vw, box.y, fit(cmp.b, vw), { fg: t.dim, bg: box.bg });
516
+ // changed rows first, then the rest, so the differences are on the first screen
517
+ const ordered = [...rows.filter((r) => r.changed), ...rows.filter((r) => !r.changed)];
518
+ cmp.scroll = Math.max(0, Math.min(cmp.scroll, Math.max(0, ordered.length - (box.h - 1))));
519
+ let y = box.y + 1;
520
+ for (let i = cmp.scroll; i < ordered.length && y < box.y + box.h; i++, y++) {
521
+ const r = ordered[i];
522
+ const rbg = r.changed ? t.selBg : box.bg;
523
+ c.fill(box.x, y, box.w, 1, ' ', { bg: rbg });
524
+ c.text(box.x + 1, y, fit(r.path, pw - 1), { fg: r.changed ? t.warn : t.dim, bg: rbg });
525
+ c.text(box.x + 1 + pw, y, fit(r.a, vw - 1), { fg: r.changed ? t.text : t.muted, bg: rbg, b: r.changed });
526
+ c.text(box.x + 2 + pw + vw, y, fit(r.b, vw - 1), { fg: r.changed ? t.text : t.muted, bg: rbg, b: r.changed });
527
+ }
528
+ }
529
+ // ───────────────────────────── backtest ─────────────────────────────
530
+ export function reportLines(r) {
531
+ const wr = r.winRate ? `${r.winRate.value.toFixed(1)}% (95% ${r.winRate.lo.toFixed(1)}–${r.winRate.hi.toFixed(1)}%)` : '—';
532
+ return [
533
+ `${r.trades} trades · win rate ${wr} · net ${usd(r.netUsd)} (${pct(r.netPct)})`,
534
+ `max drawdown ${r.maxDrawdownPct.toFixed(1)}% · profit factor ${r.profitFactor === null ? '—' : r.profitFactor.toFixed(2)} · best ${pct(r.bestPct)} · worst ${pct(r.worstPct)}`,
535
+ `stuck ${r.stuck} · open at end ${r.openAtEnd} · candidates ${r.candidates} · rugs ${r.tail.rugs} · cost back ${r.tail.costRecovered} · net without the best trade ${usd(r.tail.netWithoutBestUsd)}`,
536
+ ];
537
+ }
538
+ function renderBacktest(c, ui, a, now) {
539
+ const t = c.theme;
540
+ const subj = backtestSubject(ui);
541
+ const job = subj?.hash ? ui.backtests.get(subj.hash) : undefined;
542
+ const sw = ui.sweep && subj && ui.sweep.id === subj.id ? ui.sweep : null;
543
+ const sweepH = sw ? Math.min(11, Math.max(7, Math.floor(a.h * 0.32))) : 0;
544
+ const box = c.box(a.x, a.y, a.w, a.h - sweepH, {
545
+ title: `BACKTEST · ${subj?.id ?? 'no strategy'} · PAPER · recorded history, modelled fills`,
546
+ caption: job?.status === 'done' && job.result ? `${job.result.report.tape.mints} coins · ${job.result.report.tape.from ?? '?'} → ${job.result.report.tape.to ?? '?'}` : 'b runs it · w sweeps with a held-out check',
547
+ focus: true,
548
+ });
549
+ const bg = box.bg;
550
+ let y = box.y;
551
+ const line = (s, fg = t.text, b = false) => { if (y < box.y + box.h) {
552
+ c.text(box.x + 1, y, fit(s, box.w - 2), { fg, bg, b });
553
+ y += 1;
554
+ } };
555
+ if (!job)
556
+ line(subj ? `${subj.id} has no backtest in this session yet · b runs it on the last 14 days of recorded history` : 'select a strategy on the list', t.dim);
557
+ else if (job.status === 'running')
558
+ line(`running… ${fmtAgeSec(Math.max(0, Math.round((now - job.startedAt) / 1000)))} (off the screen loop; the app stays live)`, t.muted);
559
+ else if (job.status === 'error')
560
+ line(`✗ ${job.error}`, t.down);
561
+ const r = job?.status === 'done' ? job.result?.report : undefined;
562
+ if (r && job?.result) {
563
+ const ls = reportLines(r);
564
+ line(ls[0], r.netUsd >= 0 ? t.up : t.down, true);
565
+ line(ls[1]);
566
+ line(ls[2], t.muted);
567
+ if (r.tail.perEntry)
568
+ line(`per entry ${pct(r.tail.perEntry.meanPct)} (bootstrap 95% ${pct(r.tail.perEntry.lo)} … ${pct(r.tail.perEntry.hi)})${r.tail.multiples ? ` · exit multiples P50 ${r.tail.multiples.p50.toFixed(2)}× · P90 ${r.tail.multiples.p90.toFixed(2)}× · max ${r.tail.multiples.max.toFixed(2)}×` : ''}`, t.muted);
569
+ if (r.trades < 20)
570
+ line('n < 20: read the counts, not the rates', t.warn, true);
571
+ // equity sparkline (marked equity: stuck bags count as 0)
572
+ if (y < box.y + box.h) {
573
+ c.text(box.x + 1, y, 'equity', { fg: t.dim, bg });
574
+ sparkline(c, box.x + 9, y, Math.min(box.w - 11, 90), job.result.curve, { bg, color: r.netUsd >= 0 ? t.up : t.down });
575
+ y += 2;
576
+ }
577
+ // attribution tables, side by side
578
+ const tables = [['BY EXIT RULE', r.byExit], ['BY PROFILE', r.byProfile], ['BY TRIGGER', r.byTrigger]];
579
+ const tw = Math.floor((box.w - 2) / 3);
580
+ const ty0 = y;
581
+ let maxY = y;
582
+ tables.forEach(([title, rows], i) => {
583
+ let yy = ty0;
584
+ const x = box.x + 1 + i * tw;
585
+ if (yy < box.y + box.h)
586
+ c.text(x, yy, title, { fg: t.accent, bg, b: true });
587
+ yy += 1;
588
+ for (const [k, b] of Object.entries(rows).sort((p, q) => p[1].pnlUsd - q[1].pnlUsd).slice(0, 6)) {
589
+ if (yy >= box.y + box.h)
590
+ break;
591
+ c.text(x, yy, fit(k, tw - 18), { fg: t.muted, bg });
592
+ c.textR(x, yy, tw - 2, `${String(b.n).padStart(3)} ${usd(b.pnlUsd).padStart(10)}`, { fg: b.pnlUsd >= 0 ? t.up : t.down, bg });
593
+ yy += 1;
594
+ }
595
+ maxY = Math.max(maxY, yy);
596
+ });
597
+ y = maxY + 1;
598
+ const clamps = Object.entries(r.clamps).map(([k, n]) => `${k} ${n}`).join(' · ') || 'none';
599
+ const refusals = Object.entries(r.refusals).sort((p, q) => q[1] - p[1]).slice(0, 6).map(([k, n]) => `${k} ${n}`).join(' · ') || 'none';
600
+ line(`clamps fired: ${clamps}`, t.muted);
601
+ line(`refusals: ${refusals}`, t.muted);
602
+ y += 1;
603
+ // the data-limits box, always printed
604
+ if (y < box.y + box.h) {
605
+ const lh = Math.min(r.limits.length + 2, box.y + box.h - y);
606
+ if (lh >= 3) {
607
+ const lb = c.box(box.x + 1, y, box.w - 2, lh, { title: 'LIMITS (read before trusting any number)', bg: t.panelAlt, border: t.warn });
608
+ r.limits.slice(0, lb.h).forEach((l, i) => c.text(lb.x + 1, lb.y + i, fit(`· ${l}`, lb.w - 2), { fg: t.muted, bg: lb.bg }));
609
+ }
610
+ }
611
+ }
612
+ else if (y < box.y + box.h - 3) {
613
+ y += 1;
614
+ line('What a backtest here is: the same engine as the runner (entry rules, clamp, exits) replayed over the recorder\'s history,', t.dim);
615
+ line('with mechanical entries (no agent), modelled fills, stuck bags valued at 0, and the limits box printed with every report.', t.dim);
616
+ }
617
+ if (sw) {
618
+ const sb = c.box(a.x, a.y + a.h - sweepH, a.w, sweepH, { title: `SWEEP · ${Object.keys(sw.grid).join(' × ')} · held-out = newest 30% of coins`, caption: 'tuned on train only; the held-out coins are touched twice' });
619
+ let sy = sb.y;
620
+ const sl = (s, fg = t.text, b = false) => { if (sy < sb.y + sb.h) {
621
+ c.text(sb.x + 1, sy, fit(s, sb.w - 2), { fg, bg: sb.bg, b });
622
+ sy += 1;
623
+ } };
624
+ if (sw.status === 'running')
625
+ sl(`running… ${fmtAgeSec(Math.max(0, Math.round((now - sw.startedAt) / 1000)))}`, t.muted);
626
+ else if (sw.status === 'error')
627
+ sl(`✗ ${sw.error}`, t.down);
628
+ else if (sw.result) {
629
+ const s = sw.result;
630
+ const gain = /^held-out gain/.test(s.verdict);
631
+ sl(`VERDICT: ${s.verdict}`, gain ? t.up : t.warn, true);
632
+ sl(`split: train ${s.split.trainMints} coins · held-out ${s.split.testMints} coins (newest, from ${s.split.cutoff}) · ${s.combos} combinations run · ${s.invalid.length} refused as invalid`, t.muted);
633
+ const pw = Math.max(9, ...[s.baseline, s.best, ...s.top].map((x) => paramText(x).length));
634
+ sl(`unchanged ${sweepRow(s.baseline, pw)}`);
635
+ sl(`train best ${sweepRow(s.best, pw)}`);
636
+ sl('top on train:', t.dim);
637
+ for (const x of s.top.slice(0, Math.max(0, sb.h - 5)))
638
+ sl(` ${sweepRow(x, pw)}`, t.dim);
639
+ }
640
+ }
641
+ }
642
+ const paramText = (x) => Object.entries(x.params).map(([k, v]) => `${k.replace(/^profile\./, '')}=${v}`).join(' ') || '(as is)';
643
+ function sweepRow(x, pw) {
644
+ return `${paramText(x).padEnd(pw)} train ${String(x.train.trades).padStart(3)} ${usd(x.train.netUsd).padStart(10)} dd ${x.train.maxDrawdownPct.toFixed(1)}%${x.test ? ` · held-out ${String(x.test.trades).padStart(3)} ${usd(x.test.netUsd).padStart(10)} dd ${x.test.maxDrawdownPct.toFixed(1)}%` : ''}`;
645
+ }
646
+ // ───────────────────────────── runner ─────────────────────────────
647
+ function renderRunner(c, ui, a, now) {
648
+ const t = c.theme;
649
+ const rr = ui.runner;
650
+ const mode = runMode(ui);
651
+ const sh = mode === 'real' ? (ui.runner.kind === 'ok' && ui.runner.snap.real?.limits ? 8 : 6) : 5;
652
+ const sbox = c.box(a.x, a.y, a.w, sh, { title: `RUNNER · ${mode === 'live' ? 'LIVE · MAINNET' : mode === 'devnet' ? 'DEVNET (live signer rehearsal)' : mode === 'real' ? 'REAL · LIVE TRADE FIRE signals to your executor (no key here)' : 'PAPER'}`, caption: `status file: ${rr.path || '~/.dexterm/autotrade/runner.json'}`, focus: true });
653
+ let y = sbox.y;
654
+ const sl = (s, fg = t.text, b = false) => { if (y < sbox.y + sbox.h) {
655
+ c.text(sbox.x + 1, y, fit(s, sbox.w - 2), { fg, bg: sbox.bg, b });
656
+ y += 1;
657
+ } };
658
+ if (rr.kind === 'none') {
659
+ sl('no autotrade runner is running', t.muted, true);
660
+ sl('the runner (headless, one per machine) writes its status here every heartbeat; this view picks it up within 2 s', t.dim);
661
+ sl('`!` kill · `p` pause entries · `f` flatten act on a running runner only; nothing on this screen reaches a wallet or the signer', t.dim);
662
+ return;
663
+ }
664
+ if (rr.kind === 'bad') {
665
+ sl(`✗ status file unreadable: ${rr.error}`, t.down, true);
666
+ sl('the view shows nothing it cannot verify', t.dim);
667
+ return;
668
+ }
669
+ const s = rr.snap;
670
+ const stale = heartbeatStale(rr, now);
671
+ const age = fmtAgeSec(Math.max(0, Math.round((now - s.heartbeatAt) / 1000)));
672
+ const stateFg = stale ? t.down : s.state === 'running' ? t.up : s.state === 'paused' ? t.warn : t.down;
673
+ sl(`${stale ? 'STALE' : s.state.toUpperCase()} · ${s.strategy.id} v${s.strategy.version} (${s.strategy.hash}) · pid ${s.pid} · heartbeat ${age} ago${stale ? ' (no heartbeat for over 30 s: treat it as down)' : ''}`, stateFg, true);
674
+ sl(`book $${s.book.equityUsd.toFixed(2)} of $${s.book.startUsd.toFixed(2)} · cash $${s.book.cashUsd.toFixed(2)} · realized today ${usd(s.book.realizedTodayUsd)} · model spend $${s.spend.todayUsd.toFixed(2)} of $${s.spend.dayCapUsd.toFixed(2)} today`);
675
+ sl(s.haltReason ? `halted: ${s.haltReason}` : 'no halt', s.haltReason ? t.down : t.dim, !!s.haltReason);
676
+ // task-1645: real mode: the account, the kill switch, the outbox, each channel's health, unconfirmed positions.
677
+ const rm = s.real;
678
+ if (rm) {
679
+ const chans = rm.channels.map((h) => `${h.name} ${h.failing ? `FAILING ×${h.failing}` : 'ok'}${h.pending ? ` (${h.pending} pending)` : ''}`).join(', ') || 'none';
680
+ const unconf = rm.positions.filter((p) => p.state === 'unconfirmed').length;
681
+ sl(`real · account ${rm.accountId} · ${rm.killed ? `KILL ON (${rm.killed})` : 'kill off'} · outbox ${rm.outboxDepth} pending · ${chans} · ${unconf} unconfirmed · today ${rm.day.buySol} of ${rm.caps.dailySol ?? 'off'} SOL`, rm.killed || rm.channels.some((h) => h.failing) || unconf ? t.warn : t.text, true);
682
+ // task-1654: the limits panel (status only: limits change on your own terminal, `dexterm autotrade limits`).
683
+ if (rm.limits) {
684
+ const L = rm.limits;
685
+ const v = (k, unit) => (typeof L[k] === 'number' ? `${L[k]}${unit}` : 'off');
686
+ const pause = rm.risk?.buyPause;
687
+ sl(`limits · ${v('perTradeSol', ' SOL')}/trade${typeof L.perTradePct === 'number' ? ` (${L.perTradePct}% of book)` : ''} · ${v('dailySol', ' SOL')}/day · ${v('buysPerDay', '')} buys · ${v('maxOpenPositions', '')} open · loss stop ${v('dailyLossSol', ' SOL')} · streak ${rm.risk?.lossStreak ?? 0}/${v('lossStreak', '')}${rm.risk?.breaker ? ' · BREAKER TRIPPED' : ''}${pause ? ` · BUYS PAUSED: ${pause}` : ''} · dexterm autotrade limits show`, pause || rm.risk?.breaker ? t.warn : t.muted, !!pause);
688
+ const ex = rm.executor;
689
+ if (ex)
690
+ sl(`executor · ${ex.alive === false ? 'SILENT' : ex.alive ? 'alive' : 'not judged'}${ex.silentSec !== null && ex.lastSeenBy !== 'run-start' ? ` · last ${ex.lastSeenBy} ${fmtAgeSec(ex.silentSec)} ago` : ''}${ex.heartbeat ? ` · ${ex.heartbeat.executor}${ex.heartbeat.dryRun ? ' (dry run)' : ''}` : ' · no heartbeat yet'}${ex.lastReceipt ? ` · receipt #${ex.lastReceipt.nonce} ${ex.lastReceipt.status}` : ''}${ex.receiptLagSec !== null ? ` · lag ${ex.receiptLagSec} s` : ''}${ex.windowMin !== null ? ` · pauses buys after ${ex.windowMin} min silent` : ''}`, ex.alive === false ? t.down : t.muted, ex.alive === false);
691
+ }
692
+ }
693
+ // task-1529: a `run --all` shows its strategies side by side above the shared panels (rows tagged [strategy]).
694
+ const fleet = ui.fleet;
695
+ let fh = 0;
696
+ if (fleet) {
697
+ const lines = fleetCompareTable(fleetCompareRows(fleet));
698
+ fh = Math.min(lines.length + 4, Math.max(5, Math.floor((a.h - sh) * 0.4)));
699
+ const tot = fleetTotals(fleet);
700
+ const g = fleet.global;
701
+ const fb = c.box(a.x, a.y + sh, a.w, fh, { title: `STRATEGIES · ${fleet.members.length} at once · PAPER`, caption: 'one strategy: dexterm autotrade pause|stop|start --strategy ID' });
702
+ const rows = lines.slice(0, Math.max(0, fb.h - 2));
703
+ rows.forEach((l, i) => {
704
+ const m = i ? fleet.members[i - 1] : null;
705
+ const fg = !m ? t.muted : m.state === 'running' ? t.text : m.state === 'paused' ? t.warn : m.state === 'error' || m.state === 'halted' ? t.down : t.dim;
706
+ c.text(fb.x + 1, fb.y + i, fit(l, fb.w - 2), { fg, bg: fb.bg, b: !m });
707
+ });
708
+ const foot = [
709
+ [`together ${usd(tot.pnlUsd)} on $${tot.startUsd.toFixed(2)} · ${tot.closed} closed · ${tot.open} open · simulated fills, not financial advice`, tot.pnlUsd >= 0 ? t.up : t.down],
710
+ [`global: ${g.liveSessions}/${g.caps.maxConcurrentSessions} sessions · $${g.today.modelUsd.toFixed(2)} of $${g.caps.modelUsdPerDay} model spend today${g.load.paused ? ' · machine busy, new sessions wait' : ''}${g.blocked ? ` · ${g.blocked}` : ''}`, g.load.paused || g.blocked ? t.warn : t.muted],
711
+ ];
712
+ foot.slice(0, Math.max(0, fb.h - rows.length)).forEach(([l, fg], i) => c.text(fb.x + 1, fb.y + rows.length + i, fit(l, fb.w - 2), { fg, bg: fb.bg }));
713
+ }
714
+ const tag = (x) => { const id = x.strategy; return typeof id === 'string' ? `[${id}] ` : ''; };
715
+ const rest = a.h - sh - fh;
716
+ const ledgerH = Math.max(4, Math.floor(rest * 0.3));
717
+ const upper = rest - ledgerH;
718
+ const third = Math.floor(a.w / 3);
719
+ const panel = (x, w, title, lines) => {
720
+ const b = c.box(x, a.y + sh + fh, w, upper, { title });
721
+ lines.slice(0, b.h).forEach(([l, fg], i) => c.text(b.x + 1, b.y + i, fit(l, b.w - 2), { fg, bg: b.bg }));
722
+ if (!lines.length)
723
+ c.text(b.x + 1, b.y, 'none', { fg: t.dim, bg: b.bg, i: true });
724
+ };
725
+ panel(a.x, third, `CANDIDATES · ${s.candidates.length}`, s.candidates.map((k) => [`${tag(k)}${k.symbol.padEnd(9)} ${fmtAgeSec(k.ageSec).padStart(4)} ${k.matched.join(' · ')}${k.session ? ' ◆ session' : ''}`, t.text]));
726
+ panel(a.x + third, third, `SESSIONS · ${s.sessions.length}`, s.sessions.flatMap((k) => [
727
+ [`${tag(k)}${k.symbol.padEnd(9)} ${String(k.turns).padStart(3)} turns $${k.costUsd.toFixed(2)} of $${k.budgetUsd.toFixed(2)}`, k.costUsd > k.budgetUsd * 0.8 ? t.warn : t.text],
728
+ [` last: ${k.lastDecision ?? '—'}`, t.muted],
729
+ ]));
730
+ panel(a.x + 2 * third, a.w - 2 * third, `POSITIONS · ${s.positions.length}`, s.positions.flatMap((p) => [
731
+ [`${tag(p)}${p.symbol.padEnd(8)}${pct(p.pnlPct).padStart(8)}${usd(p.pnlUsd).padStart(9)}${p.freeBag ? ' free bag' : ''}${p.confirm ? ` · ${p.confirm}` : ''}`, p.pnlUsd >= 0 ? t.up : t.down],
732
+ [` ${p.profile} · next: ${p.next ?? '—'}`, t.muted],
733
+ ]));
734
+ const lb = c.box(a.x, a.y + sh + fh + upper, a.w, ledgerH, { title: 'RUN LEDGER · newest last', caption: 'hash-chained; every step with its exact inputs' });
735
+ const tail = s.ledgerTail.slice(-lb.h);
736
+ tail.forEach((e, i) => {
737
+ const ago = fmtAgeSec(Math.max(0, Math.round((now - e.t) / 1000)));
738
+ c.text(lb.x + 1, lb.y + i, fit(`#${String(e.seq).padEnd(6)} ${ago.padStart(4)} ${e.kind.padEnd(16)} ${e.text}`, lb.w - 2), { fg: /refus|halt|kill|rug/.test(e.kind) ? t.warn : t.muted, bg: lb.bg });
739
+ });
740
+ }
741
+ function confirmCard(c) {
742
+ const t = c.theme;
743
+ const w = Math.min(64, c.w - 4), h = 6;
744
+ const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
745
+ c.noRain(x, y, w, h);
746
+ const b = c.box(x, y, w, h, { title: 'FLATTEN · PAPER', focus: true, bg: t.panelAlt, border: t.warn });
747
+ c.text(b.x + 1, b.y, fit('Sell every paper position of this runner at the next tick.', b.w - 2), { fg: t.text, bg: b.bg });
748
+ c.text(b.x + 1, b.y + 1, fit('New entries stop first; mechanical exits keep running.', b.w - 2), { fg: t.muted, bg: b.bg });
749
+ c.text(b.x + 1, b.y + 3, 'y flatten · any other key cancels', { fg: t.warn, bg: b.bg, b: true });
750
+ }
751
+ // ───────────────────────────── journey (task-1647) ─────────────────────────────
752
+ function renderJourneyView(c, ui, a, now) {
753
+ const t = c.theme;
754
+ refreshJourney(ui);
755
+ const j = ui.journey;
756
+ const box = c.box(a.x, a.y, a.w, a.h, { title: `JOURNEY · ${j?.id ?? '—'}`, caption: 'draft → backtested → forward-tested → always-on (paper) → real · one rung at a time, on these exact rules', focus: true });
757
+ const bg = box.bg;
758
+ if (!j) {
759
+ c.text(box.x + 1, box.y, 'J on a strategy opens its journey', { fg: t.dim, bg });
760
+ return;
761
+ }
762
+ const tone = { text: t.text, ok: t.up, warn: t.warn, dim: t.dim };
763
+ // tables keep their columns: only a line wider than the box wraps
764
+ const lines = journeyLines(j, now).flatMap((l) => (l.text.length <= box.w - 2 ? [l] : wrap(l.text, box.w - 2).map((x) => ({ text: x, tone: l.tone }))));
765
+ lines.slice(0, box.h).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l.text, box.w - 2), { fg: tone[l.tone], bg }));
766
+ }
767
+ function promoteCard(c, ui, proposalId) {
768
+ const t = c.theme;
769
+ const p = ui.journey?.pending.find((x) => x.proposalId === proposalId);
770
+ const body = p ? p.card : [`promotion ${proposalId}`];
771
+ const w = Math.min(c.w - 4, 104), h = Math.min(c.h - 4, body.length + 5);
772
+ const x = Math.floor((c.w - w) / 2), y = Math.floor((c.h - h) / 2);
773
+ const box = c.box(x, y, w, h, { title: 'APPLY PROMOTION? (paper: the always-on service)', focus: true });
774
+ body.slice(0, h - 4).forEach((l, i) => c.text(box.x + 1, box.y + i, fit(l, box.w - 2), { fg: t.text, bg: box.bg }));
775
+ c.text(box.x + 1, box.y + h - 3, fit(`proposed by ${p?.actor ?? '?'} · y applies (autotrade.json gets the paper member) · any other key cancels`, box.w - 2), { fg: t.warn, bg: box.bg, b: true });
776
+ }