dexterm-cli 0.1.4 → 0.1.6

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 (131) hide show
  1. package/dist/packages/agents/src/index.js +2 -0
  2. package/dist/packages/agents/src/spectator.js +54 -0
  3. package/dist/packages/agents/src/story.js +523 -0
  4. package/dist/packages/agents/src/tiers.js +1 -0
  5. package/dist/packages/agents/src/tools.js +1 -0
  6. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
  7. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
  8. package/dist/packages/cli/src/agent-cmd.js +6 -4
  9. package/dist/packages/cli/src/agent-paper.js +60 -0
  10. package/dist/packages/cli/src/agent-spectator.js +17 -1
  11. package/dist/packages/cli/src/analyst/backend.js +206 -0
  12. package/dist/packages/cli/src/analyst/codex.js +301 -0
  13. package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
  14. package/dist/packages/cli/src/analyst/session.js +21 -3
  15. package/dist/packages/cli/src/app-help-gen.js +21 -12
  16. package/dist/packages/cli/src/backtest-cmd.js +13 -2
  17. package/dist/packages/cli/src/display.js +55 -9
  18. package/dist/packages/cli/src/doctor.js +58 -2
  19. package/dist/packages/cli/src/live/dev-history.js +115 -0
  20. package/dist/packages/cli/src/live/index.js +5 -1
  21. package/dist/packages/cli/src/live/newpairs.js +12 -0
  22. package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
  23. package/dist/packages/cli/src/live/screens/token-live.js +51 -11
  24. package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
  25. package/dist/packages/cli/src/live/token.js +1 -0
  26. package/dist/packages/cli/src/live/trade.js +22 -0
  27. package/dist/packages/cli/src/main.js +267 -19
  28. package/dist/packages/cli/src/mcp-follow.js +137 -10
  29. package/dist/packages/cli/src/plan-cmd.js +15 -5
  30. package/dist/packages/cli/src/popup.js +17 -1
  31. package/dist/packages/cli/src/record-cmd.js +2 -0
  32. package/dist/packages/cli/src/rules.js +25 -2
  33. package/dist/packages/cli/src/scan/ui.js +7 -1
  34. package/dist/packages/cli/src/screens/agent-hud.js +741 -0
  35. package/dist/packages/cli/src/screens/analyst-session.js +9 -3
  36. package/dist/packages/cli/src/screens/analyst.js +4 -3
  37. package/dist/packages/cli/src/screens/brain.js +608 -0
  38. package/dist/packages/cli/src/screens/newpairs.js +1 -1
  39. package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
  40. package/dist/packages/cli/src/screens/screener.js +1 -0
  41. package/dist/packages/cli/src/screens/token.js +16 -7
  42. package/dist/packages/cli/src/screens/trending.js +1 -1
  43. package/dist/packages/cli/src/state.js +69 -5
  44. package/dist/packages/cli/src/version.js +1 -1
  45. package/dist/packages/custom/src/rules.js +2 -2
  46. package/dist/packages/custom/src/schema.js +13 -3
  47. package/dist/packages/data/src/fixtures.js +28 -6
  48. package/dist/packages/data/src/index.js +5 -0
  49. package/dist/packages/data/src/larp/score.js +220 -0
  50. package/dist/packages/data/src/larp/x-account.js +316 -0
  51. package/dist/packages/data/src/sources/jupiter.js +1 -0
  52. package/dist/packages/data/src/trade/agent/paper-desk.js +401 -0
  53. package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
  54. package/dist/packages/data/src/trade/plans/engine.js +7 -0
  55. package/dist/packages/data/src/trade/plans/mandate.js +28 -15
  56. package/dist/packages/data/src/trade/plans/service.js +1 -1
  57. package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
  58. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
  59. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
  60. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
  61. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
  62. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
  63. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
  64. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
  65. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +4 -0
  66. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
  67. package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
  68. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +51 -0
  69. package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
  71. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  72. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
  73. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
  74. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
  75. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  76. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
  77. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
  78. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
  80. package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
  83. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  84. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
  85. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
  86. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -2
  87. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
  88. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
  89. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
  90. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
  93. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
  94. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
  95. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
  96. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
  98. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +47 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  101. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
  105. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
  106. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
  107. package/dist/packages/mcp/src/index.js +5 -2
  108. package/dist/packages/mcp/src/paper-agent.js +93 -0
  109. package/dist/packages/mcp/src/server.js +354 -19
  110. package/dist/packages/mcp/src/spectator-sink.js +158 -4
  111. package/dist/packages/mcp/src/user-content.js +1 -0
  112. package/dist/packages/packs/src/index.js +1 -0
  113. package/dist/packages/packs/src/larp-score-backtest.js +119 -0
  114. package/dist/packages/popup/src/pages/chart-lib.js +36 -15
  115. package/dist/packages/popup/src/pages/chart.css +9 -0
  116. package/dist/packages/popup/src/pages/chart.html +4 -1
  117. package/dist/packages/popup/src/pages/chart.js +34 -5
  118. package/dist/packages/popup/src/pages/spectator.css +45 -0
  119. package/dist/packages/popup/src/pages/spectator.html +1 -0
  120. package/dist/packages/popup/src/pages/spectator.js +69 -0
  121. package/dist/packages/popup/src/protocol.js +2 -0
  122. package/dist/packages/popup/src/server.js +5 -1
  123. package/dist/packages/recorder/src/dev-history.js +133 -0
  124. package/dist/packages/recorder/src/index.js +1 -0
  125. package/dist/packages/recorder/src/recorder.js +23 -0
  126. package/dist/packages/recorder/src/schema.js +11 -0
  127. package/dist/packages/tui/src/canvas.js +7 -0
  128. package/dist/packages/tui/src/fmt.js +53 -0
  129. package/dist/packages/tui/src/keymap.js +4 -0
  130. package/dist/packages/tui/src/widgets.js +9 -4
  131. package/package.json +1 -1
@@ -0,0 +1,741 @@
1
+ // `dexterm` TUI — spectator v2 (task-1466): the AGENT HUD a terminal draws while it follows an agent driving
2
+ // dexterm (`--spectate-mcp`). Instead of a list of tool calls, the watcher sees the agent WORK, in the app's own
3
+ // cinematic vocabulary:
4
+ // * the DOCK (bottom rows, below the app's footer, so the screen loses no data): a heartbeat ECG + signal line,
5
+ // the phase rail SCANNING > VETTING > SIZING > EXECUTING > HOLDING > EXITING, the PAPER badge and the pause /
6
+ // kill keys, the thought ribbon (the agent's own words, typed in), and the call timeline (nodes + result chips);
7
+ // * OVERLAYS on the screen: the rows the agent scanned glow (a beam sweeps each one), the agent cursor jumps to
8
+ // the row it opens, the token screen assembles top-down, a vetting card fills its checks and sweeps the LARP
9
+ // gauge, the PAPER ticket assembles, the fill lands with a flash, the position panel ticks the mark PnL with a
10
+ // big number + trace, exits flash, the result card resolves, and a kill is a hard red cut.
11
+ // Pure: (canvas, StoryView, opts) → cells. Every number comes from the story (app-computed); nothing here can act.
12
+ // Reduced motion (calm mode / DEXTERM_REDUCED_MOTION) drops every sweep, flash, glitch and typewriter: the same
13
+ // information appears at once. NO_COLOR keeps meaning in glyphs (▸ active phase, ✓ ! ✗ chips, ▶ cursor).
14
+ import { mix, hash3, KATAKANA, decryptText, fmtCompactUsd } from '../../../tui/src/index.js';
15
+ import { STORY_RAIL, fmtQty, fmtPx } from '../../../agents/src/index.js';
16
+ import { pnlChipVM, renderPnlChip } from "./pnl-chip.js";
17
+ /** Rows the dock takes at the bottom of the terminal. */
18
+ export const DOCK_ROWS = 5;
19
+ /** Below this terminal height the HUD collapses to the dock's rail + ribbon only (and no overlays). */
20
+ export const HUD_MIN_H = 24;
21
+ const PHASE_LABEL = {
22
+ idle: ['IDLE', 'IDLE'], scanning: ['SCANNING', 'SCAN'], vetting: ['VETTING', 'VET'], sizing: ['SIZING', 'SIZE'],
23
+ executing: ['EXECUTING', 'EXEC'], holding: ['HOLDING', 'HOLD'], exiting: ['EXITING', 'EXIT'],
24
+ };
25
+ export function phaseColor(t, p) {
26
+ switch (p) {
27
+ case 'scanning': return t.accent;
28
+ case 'vetting': return t.borderFocus;
29
+ case 'sizing': return t.meteora;
30
+ case 'executing': return t.up;
31
+ case 'holding': return t.magenta;
32
+ case 'exiting': return t.ai;
33
+ default: return t.dim;
34
+ }
35
+ }
36
+ const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
37
+ const easeOut = (x) => 1 - (1 - clamp01(x)) ** 3;
38
+ const fmtMs = (ms) => (ms == null ? '·' : ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
39
+ const signPct = (p, d = 1) => `${p >= 0 ? '+' : '−'}${Math.abs(p * 100).toFixed(d)}%`;
40
+ const signSol = (s) => `${s >= 0 ? '+' : '−'}${Math.abs(s).toFixed(4)} SOL`;
41
+ function fmtDur(ms) {
42
+ const s = Math.max(0, Math.round(ms / 1000));
43
+ if (s < 60)
44
+ return `${s}s`;
45
+ const m = Math.floor(s / 60);
46
+ return m < 60 ? `${m}m${String(s % 60).padStart(2, '0')}s` : `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m`;
47
+ }
48
+ /** Mix the background of a rectangle toward a colour (overlay tint; drawn after the effects). */
49
+ function tintRect(c, x, y, w, h, color, amt, fgToo = 0) {
50
+ if (amt <= 0)
51
+ return;
52
+ for (let yy = Math.max(0, y); yy < Math.min(c.h, y + h); yy++) {
53
+ for (let xx = Math.max(0, x); xx < Math.min(c.w, x + w); xx++) {
54
+ const i = c.idx(xx, yy);
55
+ c.bg[i] = mix(c.bg[i], color, amt);
56
+ if (fgToo)
57
+ c.fg[i] = mix(c.fg[i], color, fgToo);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Big text: a 5-row, full-block font (the boot logo's vocabulary). Full blocks only, so it renders solid in every
63
+ * terminal and in the cell-exact HTML capture (half blocks show gaps there). Digits, sign, %, . and a few letters.
64
+ */
65
+ const BIG = {
66
+ '0': [' ███ ', '█ █', '█ █', '█ █', ' ███ '],
67
+ '1': [' █ ', ' ██ ', ' █ ', ' █ ', ' ███ '],
68
+ '2': ['████ ', ' █', ' ███ ', '█ ', '█████'],
69
+ '3': ['████ ', ' █', ' ███ ', ' █', '████ '],
70
+ '4': ['█ █', '█ █', '█████', ' █', ' █'],
71
+ '5': ['█████', '█ ', '████ ', ' █', '████ '],
72
+ '6': [' ███ ', '█ ', '████ ', '█ █', ' ███ '],
73
+ '7': ['█████', ' █', ' █ ', ' █ ', ' █ '],
74
+ '8': [' ███ ', '█ █', ' ███ ', '█ █', ' ███ '],
75
+ '9': [' ███ ', '█ █', ' ████', ' █', ' ███ '],
76
+ '+': [' ', ' █ ', '█████', ' █ ', ' '],
77
+ '-': [' ', ' ', '█████', ' ', ' '],
78
+ '−': [' ', ' ', '█████', ' ', ' '],
79
+ '.': [' ', ' ', ' ', ' ', '██'],
80
+ '%': ['██ █', '██ █ ', ' █ ', ' █ ██', '█ ██'],
81
+ ' ': [' ', ' ', ' ', ' ', ' '],
82
+ 'K': ['█ █', '█ █ ', '███ ', '█ █ ', '█ █'],
83
+ 'I': ['███', ' █ ', ' █ ', ' █ ', '███'],
84
+ 'L': ['█ ', '█ ', '█ ', '█ ', '█████'],
85
+ 'E': ['█████', '█ ', '████ ', '█ ', '█████'],
86
+ 'D': ['████ ', '█ █', '█ █', '█ █', '████ '],
87
+ };
88
+ export const BIG_ROWS = 5;
89
+ /** Draw big text (5 rows) with a soft top-to-bottom ramp. Returns the width drawn. */
90
+ export function bigText(c, x, y, str, fg, _bg) {
91
+ let cx = x;
92
+ for (const ch of str) {
93
+ const g = BIG[ch] ?? BIG[' '];
94
+ for (let r = 0; r < BIG_ROWS; r++) {
95
+ const col = mix(fg, '#ffffff', r === 0 ? 0.28 : r === 1 ? 0.12 : 0);
96
+ // ink = the cell BACKGROUND (fills the whole cell in every renderer, no line gaps) + a █ glyph for NO_COLOR
97
+ for (let k = 0; k < g[r].length; k++)
98
+ if (g[r][k] !== ' ')
99
+ c.put(cx + k, y + r, '█', { fg: col, bg: col, soft: true });
100
+ }
101
+ cx += g[0].length + 1;
102
+ }
103
+ return Math.max(0, cx - x - 1);
104
+ }
105
+ export const bigTextWidth = (str) => Math.max(0, [...str].reduce((s, ch) => s + (BIG[ch] ?? BIG[' '])[0].length + 1, 0) - 1);
106
+ /** A floating card: tinted fill, rounded (or double) border in `color`, a title set into the top edge. */
107
+ function card(c, x, y, w, h, title, color, opts = {}) {
108
+ const t = c.theme;
109
+ const bg = mix(t.bg, color, opts.fill ?? 0.07);
110
+ c.fill(x, y, w, h, ' ', { bg });
111
+ const [tl, tr, bl, br, hz, vt] = opts.double ? ['╔', '╗', '╚', '╝', '═', '║'] : ['╭', '╮', '╰', '╯', '─', '│'];
112
+ const st = { fg: color, bg, soft: true };
113
+ c.put(x, y, tl, st);
114
+ c.put(x + w - 1, y, tr, st);
115
+ c.put(x, y + h - 1, bl, st);
116
+ c.put(x + w - 1, y + h - 1, br, st);
117
+ c.hline(x + 1, y, w - 2, st, hz);
118
+ c.hline(x + 1, y + h - 1, w - 2, st, hz);
119
+ c.vline(x, y + 1, h - 2, st, vt);
120
+ c.vline(x + w - 1, y + 1, h - 2, st, vt);
121
+ c.text(x + 2, y, ` ${title} `, { fg: t.bg, bg: color, b: true }, w - 4);
122
+ if (opts.right) {
123
+ const r = ` ${opts.right} `;
124
+ c.text(x + w - 2 - r.length, y, r, { fg: opts.rightColor ?? color, bg, b: true });
125
+ }
126
+ return bg;
127
+ }
128
+ // ---------------------------------------------------------------------------------------------------------------
129
+ // The dock
130
+ export function renderAgentDock(c, v, o, y0) {
131
+ const t = c.theme;
132
+ const rows = Math.min(DOCK_ROWS, c.h - y0);
133
+ if (rows <= 0)
134
+ return;
135
+ const dockBg = mix(t.bg, t.headBg, 0.6);
136
+ c.fill(0, y0, c.w, rows, ' ', { bg: dockBg });
137
+ signalRow(c, v, o, y0, dockBg);
138
+ if (rows > 1)
139
+ railRow(c, v, o, y0 + 1);
140
+ if (rows > 2)
141
+ ribbonRow(c, v, o, y0 + 2, dockBg);
142
+ if (rows > 4)
143
+ timelineRows(c, v, o, y0 + 3, dockBg);
144
+ }
145
+ /** Row 0: heartbeat ECG at the left, then a hairline with a travelling pulse, the data label at the right. */
146
+ function signalRow(c, v, o, y, bg) {
147
+ const t = c.theme;
148
+ const dead = v.control === 'killed';
149
+ const held = v.control === 'paused' || v.control === 'takeover';
150
+ const ecgW = Math.min(22, Math.floor(c.w / 6));
151
+ const beatMs = v.bpm > 0 ? 60_000 / v.bpm : Infinity;
152
+ const ecgColor = dead ? t.down : held ? t.warn : t.up;
153
+ const LV = ' ▁▂▃▄▅▆▇█';
154
+ for (let i = 0; i < ecgW; i++) {
155
+ let lv = 1;
156
+ if (!dead && !held && Number.isFinite(beatMs)) {
157
+ const ti = (o.reduced ? 0 : o.now) - (ecgW - 1 - i) * 55;
158
+ const p = ((ti % beatMs) + beatMs) % beatMs / beatMs;
159
+ lv = p < 0.035 ? 8 : p < 0.06 ? 0 : p < 0.1 ? 3 : p > 0.32 && p < 0.42 ? 2 : 1;
160
+ }
161
+ const fade = o.reduced ? 1 : 0.55 + 0.45 * (i / Math.max(1, ecgW - 1));
162
+ c.put(1 + i, y, dead || held ? '─' : LV[lv], { fg: mix(bg, ecgColor, fade), bg, soft: true });
163
+ }
164
+ const lx = ecgW + 2;
165
+ const right = o.dataLabel ? ` ${o.dataLabel} ` : '';
166
+ const lw = Math.max(0, c.w - lx - right.length - 2);
167
+ const pulseAt = o.reduced || dead || held ? -99 : Math.floor(((o.now % 1600) / 1600) * lw);
168
+ const lineColor = dead ? mix(t.bg, t.down, 0.5) : mix(t.border, phaseColor(t, v.phase), 0.55);
169
+ for (let i = 0; i < lw; i++) {
170
+ const d = pulseAt - i;
171
+ const glow = d >= 0 && d < 10 ? 1 - d / 10 : 0;
172
+ c.put(lx + i, y, glow > 0.5 ? '━' : '─', { fg: glow > 0 ? mix(lineColor, mix(phaseColor(t, v.phase), '#ffffff', 0.3), glow) : lineColor, bg, soft: true });
173
+ }
174
+ if (right)
175
+ c.text(c.w - right.length - 1, y, right, { fg: t.bg, bg: t.warn, b: true });
176
+ // task-1473: the persistent POSITION PnL chip (every screen while the agent holds), right of the hairline
177
+ const chip = o.chip !== undefined ? o.chip : pnlChipVM(v, o.now);
178
+ if (chip)
179
+ renderPnlChip(c, c.w - right.length - 2, y, Math.max(0, lw - 6), chip, { now: o.now, reduced: o.reduced });
180
+ }
181
+ /** Row 1: agent badge, heartbeat, the phase rail, PAPER, and the control keys. */
182
+ function railRow(c, v, o, y) {
183
+ const t = c.theme;
184
+ const bg = t.headBg;
185
+ c.fill(0, y, c.w, 1, ' ', { bg });
186
+ const compact = c.w < 150;
187
+ const pcol = phaseColor(t, v.phase);
188
+ const beatMs = v.bpm > 0 ? 60_000 / v.bpm : Infinity;
189
+ const beatP = Number.isFinite(beatMs) && !o.reduced ? ((o.now % beatMs) / beatMs) : 0.5;
190
+ const beatGlow = v.control === 'running' && !o.reduced ? Math.max(0, 1 - beatP * 5) : 0;
191
+ let x = 1;
192
+ x = c.text(x, y, '◆', { fg: v.control === 'killed' ? t.down : mix(pcol, '#ffffff', beatGlow * 0.5), bg, b: true });
193
+ x = c.text(x + 1, y, 'AGENT', { fg: t.dim, bg });
194
+ x = c.text(x + 1, y, v.agent.slice(0, compact ? 12 : 20), { fg: t.text, bg, b: true });
195
+ const heart = v.control === 'killed' ? '♡' : '♥';
196
+ const hcol = v.control === 'killed' ? t.down : v.control === 'running' ? mix(mix(t.down, bg, 0.35), t.down, beatGlow) : t.warn;
197
+ x = c.text(x + 2, y, heart, { fg: hcol, bg, b: true });
198
+ x = c.text(x + 1, y, v.control === 'killed' ? '--' : String(v.bpm).padStart(2, ' '), { fg: v.control === 'killed' ? t.down : t.muted, bg });
199
+ x += 3;
200
+ // the rail (dark after a kill: nothing is moving any more)
201
+ const ai = v.control === 'killed' ? -1 : STORY_RAIL.indexOf(v.phase);
202
+ const sweep = o.reduced ? 1 : clamp01((o.now - v.phaseSince) / 320);
203
+ for (let i = 0; i < STORY_RAIL.length; i++) {
204
+ const p = STORY_RAIL[i];
205
+ const label = PHASE_LABEL[p][compact ? 1 : 0];
206
+ const col = phaseColor(t, p);
207
+ if (i > 0) {
208
+ const lit = ai >= 0 && i <= ai;
209
+ x = c.text(x, y, compact ? '─' : '──', { fg: lit ? mix(t.border, col, 0.7) : mix(bg, t.border, 0.8), bg, soft: true });
210
+ }
211
+ if (i === ai) {
212
+ const s = `▸${label}`;
213
+ const cellBg = mix(bg, col, 0.28);
214
+ const start = x;
215
+ x = c.text(x, y, ` ${s} `, { fg: col, bg: cellBg, b: true });
216
+ if (sweep < 1) {
217
+ const sx = start + Math.floor(sweep * (s.length + 2));
218
+ for (let k = start; k < x; k++) {
219
+ const i2 = c.idx(k, y);
220
+ c.bg[i2] = k <= sx ? mix(cellBg, col, k === sx ? 0.6 : 0) : bg;
221
+ if (k > sx)
222
+ c.fg[i2] = mix(bg, col, 0.4);
223
+ }
224
+ }
225
+ if (p === 'holding' && v.sub)
226
+ x = c.text(x, y, `·${PHASE_LABEL[v.sub][1].toLowerCase()}`, { fg: mix(bg, phaseColor(t, v.sub), 0.8), bg });
227
+ }
228
+ else {
229
+ const passed = ai >= 0 && i < ai;
230
+ x = c.text(x, y, ` ${label} `, { fg: passed ? mix(t.dim, col, 0.45) : mix(bg, t.dim, 0.75), bg });
231
+ }
232
+ }
233
+ if (v.phase === 'idle')
234
+ x = c.text(x + 2, y, 'waiting for the agent', { fg: t.dim, bg });
235
+ // right cluster: PAPER (always visible) + control state + keys
236
+ const segs = [];
237
+ segs.push([' PAPER ', { fg: t.bg, bg: t.warn, b: true }]);
238
+ segs.push([' ', { fg: t.text, bg }]);
239
+ if (v.control === 'killed')
240
+ segs.push([' ■ AGENT KILLED ', { fg: '#ffffff', bg: t.down, b: true }]);
241
+ else {
242
+ if (v.control === 'paused')
243
+ segs.push([' ❚❚ PAUSED ', { fg: t.bg, bg: t.warn, b: true }]);
244
+ if (v.control === 'takeover')
245
+ segs.push([' YOU ARE DRIVING ', { fg: t.bg, bg: t.accent, b: true }]);
246
+ segs.push([' ', { fg: t.text, bg }]);
247
+ segs.push([' ^P ', { fg: t.bg, bg: t.accent, b: true }]);
248
+ segs.push([v.control === 'running' ? ' pause ' : ' resume ', { fg: t.muted, bg }]);
249
+ segs.push([' ^K ', { fg: '#ffffff', bg: t.down, b: true }]);
250
+ segs.push([' KILL ', { fg: t.down, bg, b: true }]);
251
+ }
252
+ const rw = segs.reduce((s, [txt]) => s + txt.length, 0);
253
+ let rx = Math.max(x + 1, c.w - rw - 1);
254
+ for (const [txt, st] of segs)
255
+ rx = c.text(rx, y, txt, st);
256
+ }
257
+ /** Row 2: the thought ribbon (agent words in coral, typed in; dexterm narration muted when the agent is quiet). */
258
+ function ribbonRow(c, v, o, y, bg) {
259
+ const t = c.theme;
260
+ const agentT = [...v.thoughts].reverse().find((x) => x.source === 'agent');
261
+ const ours = [...v.thoughts].reverse().find((x) => x.source === 'dexterm');
262
+ // The agent's own words win while fresh; otherwise dexterm narrates what the call did.
263
+ const th = agentT && (!ours || agentT.t >= ours.t - 1500 || o.now - agentT.t < 9000) ? agentT : ours ?? agentT;
264
+ const meta = `${v.calls} call${v.calls === 1 ? '' : 's'}${v.lastCallAt !== null ? ` · last ${fmtDur(o.now - v.lastCallAt)} ago` : ''}`;
265
+ const metaX = c.w - meta.length - 2;
266
+ c.text(metaX, y, meta, { fg: mix(bg, t.dim, 0.8), bg });
267
+ if (!th) {
268
+ c.text(2, y, '› the agent has not said anything yet', { fg: mix(bg, t.dim, 0.7), bg });
269
+ return;
270
+ }
271
+ const isAgent = th.source === 'agent';
272
+ const col = isAgent ? t.ai : t.muted;
273
+ const lead = isAgent ? '❯ ' : '› ';
274
+ let x = c.text(2, y, lead, { fg: col, bg, b: true });
275
+ if (isAgent)
276
+ x = c.text(x, y, `${v.agent}: `, { fg: mix(bg, t.ai, 0.7), bg });
277
+ const maxW = Math.max(0, metaX - x - 2);
278
+ const text = th.text.length > maxW ? th.text.slice(0, Math.max(0, maxW - 1)) + '…' : th.text;
279
+ const revealed = o.reduced ? text.length : Math.floor((o.now - th.t) / 14);
280
+ if (revealed >= text.length) {
281
+ c.text(x, y, text, { fg: isAgent ? mix(t.text, t.ai, 0.25) : t.muted, bg, i: !isAgent });
282
+ }
283
+ else {
284
+ const end = decryptText(c, x, y, text, Math.max(0, revealed), Math.floor(o.now / 33), { fg: isAgent ? mix(t.text, t.ai, 0.25) : t.muted, bg }, maxW);
285
+ c.put(Math.min(end, metaX - 2), y, '▌', { fg: col, bg, soft: true });
286
+ }
287
+ }
288
+ const NODE_GLYPH = (n) => (n.status === 'refused' ? '✗' : n.paper ? '■' : /^(open_token|show_in_screener|switch_screen|switch_feed)$/.test(n.tool) ? '◈' : '●');
289
+ const CHIP_MARK = { ok: '✓', slow: '!', fail: '✗', refused: '✗' };
290
+ /** Rows 3–4: the call timeline, newest at the right; spacing follows the time between calls. */
291
+ function timelineRows(c, v, o, y, bg) {
292
+ const t = c.theme;
293
+ const nodes = v.nodes;
294
+ if (!nodes.length) {
295
+ c.text(2, y, '○ no calls yet', { fg: mix(bg, t.dim, 0.7), bg });
296
+ return;
297
+ }
298
+ const newest = nodes[nodes.length - 1];
299
+ const slide = o.reduced ? 0 : Math.round((1 - easeOut((o.now - newest.t) / 260)) * 8);
300
+ let right = c.w - 3 + slide;
301
+ const chipColor = (n) => (n.status === 'ok' ? [t.up, t.upSoft] : n.status === 'slow' ? [t.warn, t.warnSoft] : [t.down, t.downSoft]);
302
+ for (let k = nodes.length - 1; k >= 0 && right > 4; k--) {
303
+ const n = nodes[k];
304
+ const head = `${NODE_GLYPH(n)} ${n.label}${n.count > 1 ? ` ×${n.count}` : ''}`;
305
+ const chip = ` ${o.depth === 'none' ? CHIP_MARK[n.status] : ''}${n.status === 'refused' ? 'refused' : fmtMs(n.ms)} `;
306
+ const cw = Math.max(head.length, chip.length);
307
+ const x0 = right - cw + 1;
308
+ const age = o.now - n.t;
309
+ const fresh = k === nodes.length - 1 && !o.reduced ? Math.max(0, 1 - age / 500) : 0;
310
+ const edgeFade = x0 < 24 ? 0.35 + 0.65 * (Math.max(0, x0) / 24) : 1;
311
+ const pc = phaseColor(t, n.phase);
312
+ const [cf, cb] = chipColor(n);
313
+ const gcol = n.status === 'refused' || n.status === 'fail' ? t.down : n.paper ? t.warn : pc;
314
+ if (x0 >= 1) {
315
+ c.text(x0, y, NODE_GLYPH(n), { fg: mix(bg, mix(gcol, '#ffffff', fresh * 0.6), edgeFade), bg: fresh > 0 ? mix(bg, gcol, fresh * 0.5) : bg, b: true });
316
+ const lx = c.text(x0 + 2, y, n.label, { fg: mix(bg, n.paper || n.status === 'refused' ? gcol : t.text, edgeFade * (n.paper ? 1 : 0.85)), bg, b: n.paper });
317
+ if (n.count > 1)
318
+ c.text(lx, y, ` ×${n.count}`, { fg: mix(bg, pc, edgeFade), bg, b: true });
319
+ c.text(x0, y + 1, chip, { fg: mix(cb, cf, edgeFade), bg: mix(bg, cb, edgeFade), b: n.status !== 'ok' });
320
+ }
321
+ // connector to the previous (older) node: longer when more time passed
322
+ const prev = nodes[k - 1];
323
+ if (prev) {
324
+ const dt = Math.max(0, n.t - prev.t);
325
+ const len = Math.max(1, Math.min(7, 1 + Math.floor(Math.log2(1 + dt / 600))));
326
+ const changed = prev.phase !== n.phase;
327
+ for (let i = 1; i <= len + 1; i++) {
328
+ const cx = x0 - i;
329
+ if (cx < 1)
330
+ break;
331
+ const ch = changed && i === Math.ceil((len + 1) / 2) ? '┼' : '─';
332
+ c.put(cx, y, ch, { fg: mix(bg, changed && ch === '┼' ? pc : mix(t.border, pc, 0.45), edgeFade), bg, soft: true });
333
+ }
334
+ right = x0 - len - 2;
335
+ }
336
+ else
337
+ right = x0 - 2;
338
+ }
339
+ }
340
+ // ---------------------------------------------------------------------------------------------------------------
341
+ // Overlays on the screen region (rows 0 .. screenH-1)
342
+ export function renderAgentOverlays(c, v, o, screenH) {
343
+ const t = c.theme;
344
+ // 1) the kill: a hard, dramatic stop takes the whole screen for a moment.
345
+ if (v.control === 'killed') {
346
+ const age = o.now - v.controlAt;
347
+ if (age >= 0 && age < 4800) {
348
+ killCut(c, v, o, screenH, age);
349
+ return;
350
+ }
351
+ }
352
+ // 2) the rows the agent touched glow; the agent cursor marks the row it opened.
353
+ rowGlow(c, v, o, screenH);
354
+ // 3) the token screen assembles top-down right after the agent opens a coin.
355
+ if (o.screen === 'token' && v.beats.openAt !== null && !o.reduced) {
356
+ const age = o.now - v.beats.openAt;
357
+ if (age >= 0 && age < 700)
358
+ assemble(c, o, screenH, age / 700);
359
+ }
360
+ // 4) cards: result > ticket / fill > position; the vetting card while vetting.
361
+ const arc = v.arc;
362
+ const res = arc?.stage === 'closed' ? arc : null;
363
+ if (res && res.closedAt !== null && o.now - res.closedAt < 10_000)
364
+ resultCard(c, res, o, screenH);
365
+ else {
366
+ if (arc && (arc.stage === 'ticket' || arc.stage === 'sized' || arc.stage === 'refused' || (arc.stage === 'open' && arc.filledAt !== null && o.now - arc.filledAt < 2600)))
367
+ ticketCard(c, v, arc, o, screenH);
368
+ else if (arc && arc.stage === 'open' && !o.brain)
369
+ positionCard(c, v, arc, o, screenH);
370
+ if (!o.brain && v.focus && (v.phase === 'vetting' || v.phase === 'sizing' || (v.phase === 'scanning' && o.now - v.focus.since < 4000)) && !(arc && arc.stage === 'open'))
371
+ vetCard(c, v, o);
372
+ }
373
+ // 5) flashes: the fill (green), an exit (gold on profit, red on loss), a refusal (red).
374
+ if (!o.reduced) {
375
+ const flash = (at, color, peak, ms) => {
376
+ if (at === null)
377
+ return;
378
+ const age = o.now - at;
379
+ if (age >= 0 && age < ms)
380
+ tintRect(c, 0, 1, c.w, screenH - 1, color, peak * (1 - age / ms));
381
+ };
382
+ flash(v.beats.fillAt, t.up, 0.2, 420);
383
+ const lastExit = arc?.fills.filter((f) => f.side === 'sell').at(-1);
384
+ flash(v.beats.exitAt, lastExit && (lastExit.realizedSol ?? 0) < 0 ? t.down : t.meteora, 0.2, 420);
385
+ flash(v.beats.refusedAt, t.down, 0.16, 300);
386
+ }
387
+ // 6) paused / taken over: say so on the band under the header, and frame the screen.
388
+ if (v.control === 'paused' || v.control === 'takeover') {
389
+ const col = v.control === 'paused' ? t.warn : t.accent;
390
+ const label = v.control === 'paused' ? ' ❚❚ AGENT PAUSED BY YOU · its next call is held · ^P resume · ^K kill ' : ' YOU ARE DRIVING · the agent is frozen · ^P hand back · ^K kill ';
391
+ const x = Math.max(0, Math.floor((c.w - label.length) / 2));
392
+ c.text(x, 1, label, { fg: t.bg, bg: col, b: true });
393
+ for (let yy = 2; yy < screenH; yy++) {
394
+ c.put(0, yy, '▌', { fg: col, bg: c.bg[c.idx(0, yy)], soft: true });
395
+ c.put(c.w - 1, yy, '▐', { fg: col, bg: c.bg[c.idx(c.w - 1, yy)], soft: true });
396
+ }
397
+ }
398
+ }
399
+ function rowGlow(c, v, o, screenH) {
400
+ const t = c.theme;
401
+ const touched = new Map(v.touched.map((r) => [r.mint, r]));
402
+ const cursor = v.cursor;
403
+ for (const m of c.rowMarks) {
404
+ if (m.y >= screenH - 1)
405
+ continue;
406
+ const isCursor = cursor !== null && cursor.mint === m.key;
407
+ const r = touched.get(m.key);
408
+ if (!r && !isCursor)
409
+ continue;
410
+ const age = r ? o.now - r.at : Infinity;
411
+ if (r && age < 0 && !isCursor)
412
+ continue; // staggered: this row's beam has not arrived yet
413
+ if (r && r.kind === 'trade') {
414
+ tintRect(c, m.x, m.y, m.w, 1, t.magenta, 0.16);
415
+ const tag = ' PAPER ◆ ';
416
+ c.text(m.x + m.w - tag.length, m.y, tag, { fg: t.bg, bg: t.magenta, b: true });
417
+ }
418
+ else if (r && r.kind === 'scan') {
419
+ const amt = o.reduced ? (age < 6000 ? 0.12 : 0) : 0.28 * Math.exp(-age / 1600);
420
+ tintRect(c, m.x, m.y, m.w, 1, t.accent, amt);
421
+ if (!o.reduced && age < 420) {
422
+ const bx = m.x + Math.floor((age / 420) * m.w);
423
+ for (let k = 0; k < 6; k++)
424
+ tintRect(c, bx - k, m.y, 1, 1, t.accent, 0.55 * (1 - k / 6));
425
+ }
426
+ }
427
+ else if (r && r.kind === 'vet') {
428
+ tintRect(c, m.x, m.y, m.w, 1, t.borderFocus, 0.1);
429
+ }
430
+ if (isCursor) {
431
+ const jump = o.reduced ? 0 : Math.max(0, 1 - (o.now - cursor.at) / 520);
432
+ tintRect(c, m.x, m.y, m.w, 1, t.accent, 0.2 + 0.4 * jump);
433
+ c.put(m.x - 1, m.y, '▶', { fg: t.accent, bg: c.bg[c.idx(Math.max(0, m.x - 1), m.y)], b: true });
434
+ const tag = ' ◀ AGENT ';
435
+ const slide = o.reduced ? 0 : Math.round(jump * 6);
436
+ const tx = m.x + m.w - tag.length + slide;
437
+ c.text(tx, m.y, tag, { fg: t.bg, bg: t.accent, b: true }, m.x + m.w - tx);
438
+ }
439
+ }
440
+ }
441
+ function assemble(c, o, screenH, p) {
442
+ const t = c.theme;
443
+ const top = 2;
444
+ const yr = top + Math.floor(easeOut(p) * (screenH - top - 1));
445
+ for (let y = yr + 1; y < screenH - 1; y++) {
446
+ for (let x = 0; x < c.w; x++) {
447
+ const i = c.idx(x, y);
448
+ const h = hash3(x, y, Math.floor(o.now / 50)) / 4294967296;
449
+ c.bg[i] = t.bg;
450
+ if (h < 0.05) {
451
+ c.ch[i] = KATAKANA[Math.floor(h * 997) % KATAKANA.length];
452
+ c.fg[i] = mix(t.bg, t.borderFocus, 0.35);
453
+ c.at[i] = 0;
454
+ }
455
+ else {
456
+ c.ch[i] = ' ';
457
+ c.at[i] = 0;
458
+ }
459
+ }
460
+ }
461
+ tintRect(c, 0, yr, c.w, 1, t.accent, 0.55);
462
+ if (yr > top)
463
+ tintRect(c, 0, yr - 1, c.w, 1, t.accent, 0.18);
464
+ }
465
+ function vetCard(c, v, o) {
466
+ const t = c.theme;
467
+ const f = v.focus;
468
+ const w = Math.min(46, c.w - 4);
469
+ const larp = f.larp;
470
+ const all = ['price', 'tape', 'chart', 'holders', 'rug', 'dev', 'larp', 'verdict'];
471
+ let checkRows = 1;
472
+ {
473
+ let cx = 2;
474
+ for (const k of all) {
475
+ const n = k.length + 1;
476
+ if (cx + n > w - 2) {
477
+ checkRows++;
478
+ cx = 2;
479
+ }
480
+ cx += n + 2;
481
+ }
482
+ }
483
+ const h = 2 + checkRows + 1 + (larp ? 2 + Math.min(2, larp.items.length) : 0) + (f.verdict ? 1 : 0);
484
+ const x = c.w - w - 2;
485
+ const y = 3;
486
+ const bg = card(c, x, y, w, h, `VETTING $${f.symbol}`, t.borderFocus, { right: fmtDur(o.now - f.since) });
487
+ let yy = y + 1;
488
+ // checks done, as chips
489
+ let cx = x + 2;
490
+ for (const k of all) {
491
+ const done = f.checks.includes(k);
492
+ const s = `${done ? '✓' : '·'}${k}`;
493
+ if (cx + s.length > x + w - 2) {
494
+ yy++;
495
+ cx = x + 2;
496
+ }
497
+ c.text(cx, yy, s, { fg: done ? t.up : mix(bg, t.dim, 0.6), bg, b: done });
498
+ cx += s.length + 2;
499
+ }
500
+ yy += 2;
501
+ void checkRows;
502
+ if (larp) {
503
+ const sweep = o.reduced ? 1 : easeOut((o.now - larp.at) / 750);
504
+ const max = 6;
505
+ const target = Math.min(max, larp.flags) / max;
506
+ const gw = Math.max(8, w - 22);
507
+ const fillN = Math.round(sweep * target * gw);
508
+ const gcol = larp.flags === 0 ? t.up : larp.flags <= 2 ? t.warn : t.down;
509
+ let gx = c.text(x + 2, yy, 'LARP ', { fg: t.muted, bg, b: true });
510
+ gx = c.text(gx, yy, '▕', { fg: t.dim, bg });
511
+ for (let i = 0; i < gw; i++) {
512
+ const on = i < fillN;
513
+ const zone = i / gw < 1 / 6 ? t.up : i / gw < 3 / 6 ? t.warn : t.down;
514
+ c.put(gx + i, yy, on ? '█' : '░', { fg: on ? mix(zone, '#ffffff', i === fillN - 1 && sweep < 1 ? 0.5 : 0) : mix(bg, t.dim, 0.45), bg, soft: true });
515
+ }
516
+ gx = c.text(gx + gw, yy, '▏', { fg: t.dim, bg });
517
+ c.text(gx + 1, yy, `${larp.flags} flag${larp.flags === 1 ? '' : 's'}`, { fg: gcol, bg, b: true });
518
+ yy++;
519
+ if (sweep >= 1)
520
+ for (const it of larp.items.slice(0, 2)) {
521
+ c.text(x + 3, yy, `· ${it}`, { fg: t.muted, bg }, w - 6);
522
+ yy++;
523
+ }
524
+ else
525
+ yy += Math.min(2, larp.items.length);
526
+ c.text(x + 3, yy, 'mechanical evidence, not a verdict', { fg: mix(bg, t.dim, 0.7), bg, i: true }, w - 6);
527
+ yy++;
528
+ }
529
+ if (f.verdict) {
530
+ const vc = f.verdict.label === 'WATCH' ? t.up : f.verdict.label === 'AVOID' ? t.down : t.warn;
531
+ let vx = c.text(x + 2, yy, 'VERDICT ', { fg: t.muted, bg, b: true });
532
+ vx = c.text(vx, yy, ` ${f.verdict.label} `, { fg: t.bg, bg: vc, b: true });
533
+ if (f.verdict.rug != null)
534
+ c.text(vx + 1, yy, `rug heuristic ${(f.verdict.rug * 100).toFixed(0)}%`, { fg: t.muted, bg });
535
+ }
536
+ }
537
+ function ticketCard(c, v, arc, o, screenH) {
538
+ const t = c.theme;
539
+ const w = Math.min(64, c.w - 4);
540
+ const h = 11;
541
+ const x = Math.min(c.w - w - 2, Math.floor((c.w - w) / 2) + Math.floor(c.w / 8));
542
+ const y = Math.max(2, Math.floor((screenH - h) / 2) - 2);
543
+ const fill = arc.fills.find((f) => f.side === 'buy') ?? null;
544
+ const filled = arc.stage === 'open' && fill !== null;
545
+ const refused = arc.stage === 'refused';
546
+ const col = refused ? t.down : filled ? t.up : t.meteora;
547
+ const bg = card(c, x, y, w, h, 'PAPER ORDER TICKET', col, { double: true, right: refused ? 'REFUSED' : filled ? 'FILLED' : arc.sizeUsd != null ? 'SIZED' : 'OPEN', rightColor: col });
548
+ const lines = [
549
+ ['SIDE', `BUY $${arc.symbol}`, t.text],
550
+ ['SIZE', arc.sizeUsd != null ? `$${arc.sizeUsd.toFixed(2)}${v.caps ? ` cap $${v.caps.perTradeUsd.toFixed(2)}/trade · $${v.caps.totalUsd.toFixed(0)} total` : ''}` : 'sizing…', arc.sizeUsd != null ? t.text : t.dim],
551
+ ['GATE', 'YOUR keypress armed the mandate · tier hands-free', t.muted],
552
+ ['', 'no keypress per trade inside it · desk enforces caps', mix(bg, t.dim, 0.85)],
553
+ ['ENTRY', fill ? `${Math.abs(fill.sol).toFixed(4)} SOL → ${fmtQty(fill.qty)} $${arc.symbol}${fill.priceUsd != null ? ` @ $${fmtPx(fill.priceUsd)}` : ''}` : refused ? '—' : 'awaiting confirm · fresh quote at fill', fill ? t.text : t.dim],
554
+ ['IMPACT', fill?.impactPct != null ? `${fill.impactPct.toFixed(2)}%${v.caps?.maxPriceImpactPct != null ? ` max ${v.caps.maxPriceImpactPct}%` : ''}` : '—', t.muted],
555
+ ['EXITS', 'agent-managed · no resting orders on PAPER', t.muted],
556
+ ];
557
+ const t0 = arc.openedAt;
558
+ lines.forEach(([k, val, fg], i) => {
559
+ const appear = o.reduced ? Infinity : o.now - (t0 + i * 110);
560
+ if (appear < 0)
561
+ return;
562
+ const yy = y + 1 + i;
563
+ c.text(x + 2, yy, k, { fg: col, bg, b: true });
564
+ const revealed = o.reduced ? val.length : Math.floor(appear / 9);
565
+ if (revealed >= val.length)
566
+ c.text(x + 10, yy, val, { fg, bg, b: k === 'SIDE' || (k === 'ENTRY' && !!fill) }, w - 12);
567
+ else
568
+ decryptText(c, x + 10, yy, val, revealed, Math.floor(o.now / 33), { fg, bg }, w - 12);
569
+ });
570
+ const sy = y + h - 2;
571
+ if (refused) {
572
+ const s = ` ✗ REFUSED BY THE DESK · ${arc.refusal ?? 'refused'} `;
573
+ c.text(x + Math.max(2, Math.floor((w - s.length) / 2)), sy, s, { fg: '#ffffff', bg: t.down, b: true }, w - 4);
574
+ }
575
+ else if (filled) {
576
+ const age = o.now - (arc.filledAt ?? o.now);
577
+ const blink = o.reduced || age > 700 || Math.floor(age / 140) % 2 === 0;
578
+ const s = ' ✔ FILLED · PAPER · simulated ledger ';
579
+ if (blink)
580
+ c.text(x + Math.floor((w - s.length) / 2), sy, s, { fg: t.bg, bg: t.up, b: true });
581
+ }
582
+ else {
583
+ const s = arc.sizeUsd != null ? '◌ waiting for the agent to confirm' : '◌ the agent is sizing';
584
+ const pulse = o.reduced ? 0.8 : 0.5 + 0.5 * Math.sin(o.now / 180);
585
+ c.text(x + Math.floor((w - s.length) / 2), sy, s, { fg: mix(bg, t.meteora, pulse), bg });
586
+ }
587
+ }
588
+ function positionCard(c, v, arc, o, screenH) {
589
+ const t = c.theme;
590
+ const w = Math.min(62, c.w - 4);
591
+ const h = 11;
592
+ const x = c.w - w - 2;
593
+ const y = Math.max(2, screenH - h - 2);
594
+ const p = arc.pnlPct;
595
+ const col = p === null ? t.muted : p >= 0 ? t.up : t.down;
596
+ const exits = arc.fills.filter((f) => f.side === 'sell');
597
+ const lastExit = exits.at(-1);
598
+ const exitFresh = lastExit && o.now - lastExit.t < 2600;
599
+ const bg = card(c, x, y, w, h, `PAPER POSITION · $${arc.symbol}`, t.magenta, { right: exitFresh ? 'EXITING' : 'HOLDING', rightColor: exitFresh ? t.ai : t.magenta });
600
+ // the big number: the mark PnL of the open size, ticking with the app's mark price
601
+ const big = p === null ? '--' : signPct(p);
602
+ const tick = arc.mark && !o.reduced ? Math.max(0, 1 - (o.now - arc.mark.at) / 350) : 0;
603
+ bigText(c, x + 2, y + 1, big, mix(col, '#ffffff', tick * 0.45), bg);
604
+ const sx = x + 3 + bigTextWidth(big);
605
+ c.text(sx, y + 1, 'MARK PnL', { fg: t.dim, bg });
606
+ if (arc.peakPct !== null)
607
+ c.text(sx, y + 2, `peak ${signPct(arc.peakPct)}`, { fg: arc.peakPct >= 0 ? t.up : t.down, bg });
608
+ c.text(sx, y + 3, `held ${fmtDur(o.now - (arc.filledAt ?? o.now))}`, { fg: t.muted, bg });
609
+ if (exits.length)
610
+ c.text(sx, y + 4, `exits ${exits.length} · ${signSol(exits.reduce((s, e) => s + (e.realizedSol ?? 0), 0))}`, { fg: t.ai, bg }, x + w - 2 - sx);
611
+ // the trace: one cell per sample, zero line dotted
612
+ const tw = w - 4;
613
+ const tr = arc.trace.slice(-tw);
614
+ if (tr.length > 1) {
615
+ const lo = Math.min(0, ...tr.map((s) => s.pct)), hi = Math.max(0, ...tr.map((s) => s.pct));
616
+ const span = hi - lo || 1;
617
+ const LV = '▁▂▃▄▅▆▇█';
618
+ tr.forEach((s, i) => {
619
+ const lv = Math.round(((s.pct - lo) / span) * 7);
620
+ c.put(x + 2 + i, y + 6, LV[lv], { fg: s.pct >= 0 ? t.up : t.down, bg, soft: true });
621
+ });
622
+ }
623
+ else
624
+ c.text(x + 2, y + 6, '· waiting for the first mark tick', { fg: t.dim, bg });
625
+ // task-1472: with the chart axis on market cap and a known supply, entry/mark read MC (compact k/M/B), like the charts
626
+ const sc = o.mcapScale?.(arc.mint) ?? null;
627
+ const px = (p) => (sc ? `MC ${fmtCompactUsd(p * sc)}` : `$${fmtPx(p)}`);
628
+ const entry = arc.entryUsd !== null ? `entry ${px(arc.entryUsd)}` : 'entry —';
629
+ const mark = arc.mark ? `mark ${px(arc.mark.priceUsd)}` : 'mark —';
630
+ c.text(x + 2, y + 8, `${entry} · ${mark} · ${arc.spentSol.toFixed(4)} SOL in`, { fg: t.muted, bg }, w - 4);
631
+ c.text(x + 2, y + 9, 'mark = the app\'s live price · PAPER · simulated', { fg: mix(bg, t.dim, 0.7), bg, i: true }, w - 4);
632
+ // a desk refusal while holding (paused / stopped / a cap): said on the panel, in red, for a few seconds
633
+ const refusedFresh = arc.refusal && v.beats.refusedAt !== null && o.now - v.beats.refusedAt < 3500;
634
+ if (refusedFresh && !exitFresh) {
635
+ const s = ` ✗ REFUSED BY THE DESK · ${arc.refusal} `;
636
+ c.text(x + w - s.length - 2, y + 6, s, { fg: '#ffffff', bg: t.down, b: true });
637
+ }
638
+ if (exitFresh) {
639
+ const s = ` SOLD ${fmtQty(lastExit.qty)} · ${signSol(lastExit.sol)} `;
640
+ c.text(x + w - s.length - 2, y + 6, s, { fg: t.bg, bg: (lastExit.realizedSol ?? 0) >= 0 ? t.meteora : t.down, b: true });
641
+ }
642
+ }
643
+ function resultCard(c, arc, o, screenH) {
644
+ const t = c.theme;
645
+ const r = arc.result;
646
+ const win = r.realizedSol >= 0;
647
+ const col = win ? t.up : t.down;
648
+ const age = o.now - (arc.closedAt ?? o.now);
649
+ const w = Math.min(64, c.w - 4);
650
+ const h = 12;
651
+ const x = Math.floor((c.w - w) / 2);
652
+ const y = Math.max(2, Math.floor((screenH - h) / 2));
653
+ if (!o.reduced && age < 220) {
654
+ // the card opens from a line
655
+ const half = Math.max(1, Math.floor((w / 2) * easeOut(age / 220)));
656
+ c.hline(Math.floor(c.w / 2) - half, y + Math.floor(h / 2), half * 2, { fg: col, bg: c.bg[c.idx(Math.floor(c.w / 2), y + Math.floor(h / 2))], b: true }, '━');
657
+ return;
658
+ }
659
+ const bg = card(c, x, y, w, h, `PAPER RESULT · $${arc.symbol}`, col, { double: true, right: win ? 'CLOSED GREEN' : 'CLOSED RED', fill: 0.1 });
660
+ const big = r.pct === null ? signSol(r.realizedSol) : signPct(r.pct);
661
+ const bw = bigTextWidth(big);
662
+ bigText(c, x + Math.floor((w - bw) / 2), y + 2, big, col, bg);
663
+ const l1 = `${signSol(r.realizedSol)} realized · held ${fmtDur(r.holdMs)} · ${r.exits} exit${r.exits === 1 ? '' : 's'}${arc.peakPct !== null ? ` · peak ${signPct(arc.peakPct)}` : ''}`;
664
+ c.textC(x + 1, y + 8, w - 2, l1, { fg: t.text, bg, b: true });
665
+ c.textC(x + 1, y + 9, w - 2, 'simulated paper trade · not a result · not advice', { fg: mix(bg, t.dim, 0.8), bg, i: true });
666
+ }
667
+ function killCut(c, v, o, screenH, age) {
668
+ const t = c.theme;
669
+ if (!o.reduced && age < 140) {
670
+ tintRect(c, 0, 0, c.w, screenH, t.down, 0.5, 0.3);
671
+ }
672
+ else if (!o.reduced && age < 820) {
673
+ // glitch: rows slip sideways, colour drains, red bars cut through
674
+ const f = Math.floor(age / 40);
675
+ for (let y = 0; y < screenH; y++) {
676
+ const h = hash3(y, f, 77) / 4294967296;
677
+ if (h < 0.3) {
678
+ const shift = Math.floor((hash3(y, f, 11) / 4294967296) * 9) - 4;
679
+ const row = c.ch.slice(y * c.w, (y + 1) * c.w);
680
+ for (let x = 0; x < c.w; x++)
681
+ c.ch[c.idx(x, y)] = row[(x - shift + c.w) % c.w];
682
+ }
683
+ tintRect(c, 0, y, c.w, 1, t.bg, 0.45, 0.55);
684
+ if (h > 0.93)
685
+ tintRect(c, 0, y, c.w, 1, t.down, 0.55);
686
+ }
687
+ }
688
+ else {
689
+ tintRect(c, 0, 0, c.w, screenH, t.bg, 0.55, 0.62);
690
+ }
691
+ if (o.reduced || age >= 140) {
692
+ const word = 'KILLED';
693
+ const bw = bigTextWidth(word);
694
+ const w = Math.min(c.w - 4, Math.max(bw + 8, 64));
695
+ const h = 11;
696
+ const x = Math.floor((c.w - w) / 2);
697
+ const y = Math.max(1, Math.floor((screenH - h) / 2));
698
+ const bg = card(c, x, y, w, h, 'AGENT STOPPED', t.down, { double: true, fill: 0.16 });
699
+ bigText(c, x + Math.floor((w - bw) / 2), y + 2, word, t.down, bg);
700
+ c.textC(x + 1, y + 8, w - 2, `${v.agent} · session closed · agent paper desk stopped`, { fg: t.text, bg, b: true });
701
+ c.textC(x + 1, y + 9, w - 2, 'you are driving · restart to let an agent in again', { fg: t.muted, bg });
702
+ }
703
+ }
704
+ /**
705
+ * Whether the HUD has motion to show right now (the frame loop then paints at full fps instead of the idle stride):
706
+ * the agent called in the last 20 s, a PAPER position is open, a result / kill is on screen, or a line is typing.
707
+ */
708
+ export function hudAnimating(v, now) {
709
+ if (v.control === 'killed')
710
+ return now - v.controlAt < 5000;
711
+ if (v.control !== 'running')
712
+ return false;
713
+ if (v.arc && (v.arc.stage === 'open' || (v.arc.closedAt !== null && now - v.arc.closedAt < 10_000)))
714
+ return true;
715
+ if (v.lastCallAt !== null && now - v.lastCallAt < 20_000)
716
+ return true;
717
+ const last = v.thoughts[v.thoughts.length - 1];
718
+ return !!last && now - last.t < 8000;
719
+ }
720
+ /** Compose: the app's frame rendered `screenH` rows tall on top, the dock below, overlays over the screen. */
721
+ export function composeHud(full, screen, v, o) {
722
+ const n = screen.w * screen.h;
723
+ for (let i = 0; i < n; i++) {
724
+ full.ch[i] = screen.ch[i];
725
+ full.fg[i] = screen.fg[i];
726
+ full.bg[i] = screen.bg[i];
727
+ full.at[i] = screen.at[i];
728
+ full.hard[i] = screen.hard[i];
729
+ full.zone[i] = screen.zone[i];
730
+ full.shield[i] = screen.shield[i];
731
+ }
732
+ full.images = screen.images.slice();
733
+ full.rowMarks = screen.rowMarks.slice();
734
+ full.headings = screen.headings.slice();
735
+ full.shields = screen.shields.slice();
736
+ full.imageMode = screen.imageMode;
737
+ if (full.h >= HUD_MIN_H)
738
+ renderAgentOverlays(full, v, o, screen.h);
739
+ renderAgentDock(full, v, o, screen.h);
740
+ return full;
741
+ }