dexterm-cli 0.1.5 → 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 (93) 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-spectator.js +17 -1
  10. package/dist/packages/cli/src/analyst/backend.js +23 -0
  11. package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
  12. package/dist/packages/cli/src/analyst/session.js +12 -1
  13. package/dist/packages/cli/src/app-help-gen.js +7 -7
  14. package/dist/packages/cli/src/backtest-cmd.js +13 -2
  15. package/dist/packages/cli/src/display.js +55 -9
  16. package/dist/packages/cli/src/live/dev-history.js +31 -15
  17. package/dist/packages/cli/src/live/index.js +5 -1
  18. package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
  19. package/dist/packages/cli/src/live/screens/token-live.js +41 -11
  20. package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
  21. package/dist/packages/cli/src/live/trade.js +22 -0
  22. package/dist/packages/cli/src/main.js +146 -12
  23. package/dist/packages/cli/src/mcp-follow.js +103 -6
  24. package/dist/packages/cli/src/plan-cmd.js +15 -5
  25. package/dist/packages/cli/src/popup.js +17 -1
  26. package/dist/packages/cli/src/scan/ui.js +7 -1
  27. package/dist/packages/cli/src/screens/agent-hud.js +741 -0
  28. package/dist/packages/cli/src/screens/analyst-session.js +4 -0
  29. package/dist/packages/cli/src/screens/brain.js +608 -0
  30. package/dist/packages/cli/src/screens/newpairs.js +1 -1
  31. package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
  32. package/dist/packages/cli/src/screens/screener.js +1 -0
  33. package/dist/packages/cli/src/screens/token.js +16 -7
  34. package/dist/packages/cli/src/screens/trending.js +1 -1
  35. package/dist/packages/cli/src/state.js +69 -5
  36. package/dist/packages/cli/src/version.js +1 -1
  37. package/dist/packages/custom/src/schema.js +8 -2
  38. package/dist/packages/data/src/index.js +2 -0
  39. package/dist/packages/data/src/larp/score.js +220 -0
  40. package/dist/packages/data/src/larp/x-account.js +316 -0
  41. package/dist/packages/data/src/trade/agent/paper-desk.js +77 -12
  42. package/dist/packages/data/src/trade/plans/engine.js +7 -0
  43. package/dist/packages/data/src/trade/plans/mandate.js +28 -15
  44. package/dist/packages/data/src/trade/plans/service.js +1 -1
  45. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +1 -1
  46. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
  47. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
  48. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  49. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
  50. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +3 -2
  51. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
  52. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
  53. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
  54. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  55. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  56. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
  57. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
  58. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
  59. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
  60. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
  61. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
  62. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
  63. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
  64. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
  65. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
  66. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
  67. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
  68. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  69. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
  71. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
  72. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
  73. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
  74. package/dist/packages/mcp/src/paper-agent.js +3 -2
  75. package/dist/packages/mcp/src/server.js +162 -21
  76. package/dist/packages/mcp/src/spectator-sink.js +85 -0
  77. package/dist/packages/packs/src/index.js +1 -0
  78. package/dist/packages/packs/src/larp-score-backtest.js +119 -0
  79. package/dist/packages/popup/src/pages/chart-lib.js +36 -15
  80. package/dist/packages/popup/src/pages/chart.css +9 -0
  81. package/dist/packages/popup/src/pages/chart.html +4 -1
  82. package/dist/packages/popup/src/pages/chart.js +34 -5
  83. package/dist/packages/popup/src/pages/spectator.css +45 -0
  84. package/dist/packages/popup/src/pages/spectator.html +1 -0
  85. package/dist/packages/popup/src/pages/spectator.js +69 -0
  86. package/dist/packages/popup/src/protocol.js +2 -0
  87. package/dist/packages/popup/src/server.js +5 -1
  88. package/dist/packages/recorder/src/dev-history.js +7 -3
  89. package/dist/packages/tui/src/canvas.js +7 -0
  90. package/dist/packages/tui/src/fmt.js +53 -0
  91. package/dist/packages/tui/src/keymap.js +4 -0
  92. package/dist/packages/tui/src/widgets.js +9 -4
  93. package/package.json +1 -1
@@ -180,22 +180,43 @@
180
180
  if (a >= 1e3) return `${s}$${(a / 1e3).toFixed(1)}K`;
181
181
  return `${s}$${a.toFixed(0)}`;
182
182
  }
183
- // ---- task-1366 market cap (mirrors @dexterm/tui fmtMcapBare: K/M/B/T, ≤ 3 significant digits, never 1000K)
184
- const MC_UNITS = ['', 'K', 'M', 'B', 'T'];
185
- function fmtMcapAxis(n) {
183
+ // ---- task-1472 the one chart number formatter (mirrors @dexterm/tui fmtCompact, parity-tested): lowercase k, then
184
+ // M/B/T; one decimal while < 10 of the unit, none after; the unit is promoted after rounding (999 900 → 1M); below 1
185
+ // the subscript price form (0.0₅493): never scientific notation. `extra` (0..3) keeps two axis ticks apart.
186
+ const CU = ['', 'k', 'M', 'B', 'T'];
187
+ const trimZeros = (t) => (t.includes('.') ? t.replace(/0+$/, '').replace(/\.$/, '') : t);
188
+ function tinyBare(p) {
189
+ if (p >= 0.01) return p.toFixed(4);
190
+ const m = p.toFixed(12).match(/^0\.(0*)(\d+)/);
191
+ if (!m) return String(p);
192
+ const zeros = m[1].length;
193
+ const digits = m[2].replace(/0+$/, '').slice(0, 4) || '0';
194
+ if (zeros < 3) return `0.${m[1]}${digits}`;
195
+ return `0.0${String(zeros).split('').map((c) => SUB[Number(c)]).join('')}${digits}`;
196
+ }
197
+ function fmtCompact(n, extra = 0) {
186
198
  if (!fin(n)) return '—';
199
+ if (n === 0) return '0';
187
200
  const sign = n < 0 ? '-' : '';
188
- let a = Math.abs(n);
189
- if (a === 0) return '0';
190
- if (a < 1) return sign + String(Number(a.toPrecision(2)));
191
- let u = 0;
192
- while (u < MC_UNITS.length - 1 && a >= 1000) { a /= 1000; u++; }
193
- let r = Number(a.toPrecision(3));
194
- if (r >= 1000 && u < MC_UNITS.length - 1) { r = Number((r / 1000).toPrecision(3)); u++; }
195
- return `${sign}${r >= 1000 ? String(Math.round(r)) : String(r)}${MC_UNITS[u]}`;
201
+ const a = Math.abs(n);
202
+ if (a < 1) return sign + trimZeros(tinyBare(a));
203
+ const x = Math.max(0, Math.min(3, Math.floor(extra) || 0));
204
+ const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
205
+ let u = 0, v = a;
206
+ while (u < CU.length - 1 && v >= 1000) { v /= 1000; u++; }
207
+ let r = round(v);
208
+ if (r >= 1000 && u < CU.length - 1) { u++; r = round(r / 1000); }
209
+ return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${CU[u]}`;
210
+ }
211
+ /** `$4.9k`, `-$1.2k` (volume pane, legend volume). */
212
+ function fmtCompactUsd(n, extra = 0) {
213
+ const b = fmtCompact(n, extra);
214
+ return b === '—' ? b : b.startsWith('-') ? `-$${b.slice(1)}` : `$${b}`;
196
215
  }
216
+ /** task-1366 name kept: the mcap axis label is now the compact formatter. */
217
+ const fmtMcapAxis = (n) => fmtCompact(n);
197
218
  const MCAP_LABEL = 'mcap (current supply)';
198
- const MCAP_FALLBACK = 'supply unknown · showing price';
219
+ const MCAP_FALLBACK = 'price (mcap unavailable)'; // task-1472: the card's wording (was 'supply unknown · showing price')
199
220
  /**
200
221
  * How the page shows numbers for a VM: `mcap` (with the supply multiplier) only when the terminal is in mcap mode AND
201
222
  * sent a usable supply; mcap mode without one is `price` + a visible note. Never invents a supply.
@@ -207,7 +228,7 @@
207
228
  if (supply) return { mode: 'mcap', supply, label: MCAP_LABEL, key: `mcap:${Number(supply.toPrecision(4))}` };
208
229
  return { mode: 'price', supply: null, label: wantMcap ? MCAP_FALLBACK : null, key: wantMcap ? 'price:fallback' : 'price' };
209
230
  }
210
- /** The Y-axis / crosshair label formatter for a display: mcap = price × supply in K/M/B, else the price label. */
231
+ /** The Y-axis / crosshair label formatter for a display: mcap = price × supply in compact k/M/B, else the price label. */
211
232
  function axisFormatter(d) {
212
233
  if (d && d.mode === 'mcap' && fin(d.supply)) { const s = d.supply; return (p) => fmtMcapAxis(p * s); }
213
234
  return fmtAxis;
@@ -225,7 +246,7 @@
225
246
  const chg = base > 0 ? (bar.close - base) / base : null;
226
247
  const f = axisFormatter(display);
227
248
  const mc = !!display && display.mode === 'mcap';
228
- return { o: f(bar.open), h: f(bar.high), l: f(bar.low), c: mc ? `${f(bar.close)} (${fmtPrice(bar.close)})` : f(bar.close), v: fmtUsd(bar.volume), chg: fmtPct(chg), up: bar.close >= bar.open };
249
+ return { o: f(bar.open), h: f(bar.high), l: f(bar.low), c: mc ? `${f(bar.close)} (${fmtPrice(bar.close)})` : f(bar.close), v: fmtCompactUsd(bar.volume), chg: fmtPct(chg), up: bar.close >= bar.open };
229
250
  }
230
251
 
231
252
  /** Index of the bar at `time` (binary search), or -1. */
@@ -257,6 +278,6 @@
257
278
  return bars[i].time + f * (bars[i + 1].time - bars[i].time);
258
279
  }
259
280
 
260
- const api = { setTzOffset, chartTime, INTERVALS, PERIOD_MS, INDICATORS, STYLES, SCALES, toBars, mergeTail, ema, sma, vwap, volumes, barTimeOf, tapeMarkers, fillMarkers, priceFormatFor, fmtPrice, fmtAxis, fmtUsd, fmtPct, fmtSol, legendFor, fmtMcapAxis, displayOf, axisFormatter, MCAP_LABEL, MCAP_FALLBACK, indexOfTime, logicalOf, timeOfLogical };
281
+ const api = { setTzOffset, chartTime, INTERVALS, PERIOD_MS, INDICATORS, STYLES, SCALES, toBars, mergeTail, ema, sma, vwap, volumes, barTimeOf, tapeMarkers, fillMarkers, priceFormatFor, fmtPrice, fmtAxis, fmtUsd, fmtPct, fmtSol, legendFor, fmtMcapAxis, fmtCompact, fmtCompactUsd, displayOf, axisFormatter, MCAP_LABEL, MCAP_FALLBACK, indexOfTime, logicalOf, timeOfLogical };
261
282
  root.DTChart = Object.freeze(api);
262
283
  })(typeof window !== 'undefined' ? window : globalThis);
@@ -48,3 +48,12 @@
48
48
  /* LWC attribution logo: the library injects this exact rule too (allowed by CSP hash); repeated here so the
49
49
  logo stays positioned even if a future build changes its inline style text. */
50
50
  a#tv-attr-logo { position: absolute; left: 10px; bottom: 10px; height: 19px; width: 35px; margin: 0; padding: 0; border: 0; z-index: 3; }
51
+ /* task-1473 r2: the persistent POSITION PnL chip (the held coin, which need not be the charted coin). Display only. */
52
+ .pnlchip { display: flex; flex-wrap: nowrap; gap: 0; padding: 2px 10px; font-size: 11px; white-space: nowrap; overflow: hidden; border-bottom: 1px solid var(--border); background: rgba(255,45,240,.07); color: var(--muted); }
53
+ .pnlchip .seg + .seg::before { content: ' │ '; color: var(--border); }
54
+ .pnlchip .head { color: var(--pump); font-weight: 700; }
55
+ .pnlchip .pnl { font-weight: 700; }
56
+ .pnlchip .pnl.up, .pnlchip .peak.up { color: var(--up); } .pnlchip .pnl.down, .pnlchip .peak.down { color: var(--down); }
57
+ .pnlchip .dd { color: var(--warn); }
58
+ .pnlchip .pnl.flash { background: var(--up); color: var(--bg, #000); padding: 0 3px; border-radius: 2px; }
59
+ @media (prefers-reduced-motion: reduce) { .pnlchip .pnl.flash { background: none; color: var(--up); padding: 0; } }
@@ -23,6 +23,8 @@
23
23
  <div id="toolbar" class="toolbar">
24
24
  <span class="grp" id="tf"></span>
25
25
  <span class="sep"></span>
26
+ <span class="grp" id="axis"></span>
27
+ <span class="sep"></span>
26
28
  <span class="grp" id="styles"></span>
27
29
  <span class="sep"></span>
28
30
  <span class="grp" id="scales"></span>
@@ -31,6 +33,7 @@
31
33
  <span class="sep"></span>
32
34
  <span class="grp" id="draws"></span>
33
35
  </div>
36
+ <div id="pnlchip" class="pnlchip" hidden title="position PnL (display only)"></div>
34
37
  <div id="banner" class="banner" hidden></div>
35
38
  <div class="stage">
36
39
  <canvas id="rain"></canvas>
@@ -44,7 +47,7 @@
44
47
  <div id="pos-cells" class="pos-cells"></div>
45
48
  </div>
46
49
  <div id="foot" class="foot">
47
- <span class="keys">1–6 tf · c style · a/l/p scale · e ema · w vwap · m vol·ma · v vol · b tape · h/t draw · f fit · esc close</span>
50
+ <span class="keys">1–6 tf · A/y mcap⇄price · c style · a/l/p scale · e ema · w vwap · m vol·ma · v vol · b tape · h/t draw · f fit · esc close</span>
48
51
  <span class="attr" title="TradingView Lightweight Charts v__LWC_VERSION__ (Apache-2.0), self-hosted"><span id="notice">__LWC_NOTICE__</span> · <a id="tv-link" href="https://www.tradingview.com/" target="_blank" rel="noopener noreferrer">https://www.tradingview.com/</a></span>
49
52
  </div>
50
53
  </div>
@@ -12,6 +12,7 @@
12
12
  const $ = (id) => document.getElementById(id);
13
13
  const titleEl = $('title'), legendEl = $('legend'), liveEl = $('live'), bannerEl = $('banner'), hintEl = $('hint');
14
14
  const posEl = $('pos'), posTitleEl = $('pos-title'), posCapEl = $('pos-cap'), posCellsEl = $('pos-cells');
15
+ const chipEl = $('pnlchip');
15
16
  const imgSlot = $('coin-img-slot');
16
17
  const chartEl = $('chart'), drawCv = $('draw'), dctx = drawCv.getContext('2d');
17
18
  const STALE_MS = 20_000;
@@ -57,7 +58,7 @@
57
58
  if (type === 2) return String(d.getUTCDate());
58
59
  return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
59
60
  }
60
- const volFmt = (v) => C.fmtUsd(v);
61
+ const volFmt = (v) => C.fmtCompactUsd(v); // task-1472: volume stays USD, compact k/M
61
62
 
62
63
  // ---------------------------------------------------------------- chart + series
63
64
  const chart = L.createChart(chartEl, {
@@ -80,7 +81,7 @@
80
81
  handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: false },
81
82
  handleScale: { axisPressedMouseMove: { time: true, price: true }, axisDoubleClickReset: true, mouseWheel: true, pinch: true },
82
83
  });
83
- // task-1366: the axis formatter follows the terminal's price mode (mcap = price × the current supply, K/M/B)
84
+ // task-1366/1472: the axis formatter follows the terminal's chart axis (mcap = price × the current supply, compact k/M/B)
84
85
  const pricePF = (p) => ({ type: 'custom', formatter: C.axisFormatter(S.display), minMove: C.priceFormatFor(p).minMove });
85
86
  // Keep the position levels (entry / stop / target) and the live price in view: autoscale widens to include them
86
87
  // when they are within 3× of the last close (a far-away level would squash the candles; its row cell still shows it).
@@ -159,13 +160,16 @@
159
160
  DT.onTheme(applyChartTheme);
160
161
 
161
162
  // ---------------------------------------------------------------- toolbar (buttons built with textContent only)
162
- const btns = { tf: new Map(), styles: new Map(), scales: new Map(), inds: new Map(), draws: new Map() };
163
+ const btns = { tf: new Map(), axis: new Map(), styles: new Map(), scales: new Map(), inds: new Map(), draws: new Map() };
163
164
  function mkBtn(group, id, label, title, onClick) {
164
165
  const b = document.createElement('button'); b.type = 'button'; b.className = 'tb'; b.textContent = label; b.title = title;
165
166
  b.addEventListener('click', (e) => { e.preventDefault(); onClick(); });
166
167
  $(group).appendChild(b); btns[group].set(id, b); return b;
167
168
  }
168
169
  C.INTERVALS.forEach((iv, i) => mkBtn('tf', iv, iv, `timeframe ${iv} (${i + 1})`, () => requestInterval(iv)));
170
+ // task-1472: MCAP | PRICE asks the terminal to switch (and save) the chart axis; the next VM re-labels the page
171
+ mkBtn('axis', 'mcap', 'MCAP', 'Y axis: market cap (A / y toggles; saved by the terminal)', () => setAxis('mcap'));
172
+ mkBtn('axis', 'price', 'PRICE', 'Y axis: USD price (A / y toggles; saved by the terminal)', () => setAxis('price'));
169
173
  mkBtn('styles', 'candles', 'candles', 'candlesticks (c cycles)', () => setStyle('candles', true));
170
174
  mkBtn('styles', 'line', 'line', 'line (c cycles)', () => setStyle('line', true));
171
175
  mkBtn('styles', 'area', 'area', 'area (c cycles)', () => setStyle('area', true));
@@ -183,6 +187,8 @@
183
187
  mkBtn('draws', 'x', '✕ clear', 'clear drawings (x; backspace = undo last)', () => clearDrawings());
184
188
  function syncToolbar() {
185
189
  for (const [iv, b] of btns.tf) { b.classList.toggle('on', iv === S.interval); b.classList.toggle('pending', iv === S.pendingInterval && iv !== S.interval); }
190
+ const ax = S.vm && S.vm.priceMode === 'mcap' ? 'mcap' : 'price';
191
+ for (const [k, b] of btns.axis) b.classList.toggle('on', k === ax);
186
192
  for (const [k, b] of btns.styles) b.classList.toggle('on', k === S.style);
187
193
  for (const [k, b] of btns.scales) b.classList.toggle('on', k === S.scale);
188
194
  for (const [k, b] of btns.inds) b.classList.toggle('on', S.ind.has(k));
@@ -196,6 +202,11 @@
196
202
  S.pendingInterval = iv; S.reqAt = performance.now(); syncToolbar();
197
203
  DT.send({ type: 'event', name: 'chart.interval', payload: { interval: iv } });
198
204
  }
205
+ /** task-1472: ask the terminal for a Y axis; nothing changes here until its next VM carries the new mode. */
206
+ function setAxis(axis) {
207
+ if (axis !== 'mcap' && axis !== 'price') return;
208
+ DT.send({ type: 'event', name: 'chart.axis', payload: { axis } });
209
+ }
199
210
  function setStyle(st, user) {
200
211
  if (!C.STYLES.includes(st) || st === S.style) return;
201
212
  S.style = st;
@@ -397,6 +408,23 @@
397
408
  }
398
409
  }
399
410
 
411
+ // task-1473 r2: the POSITION PnL chip (formatted terminal-side; textContent only, display only)
412
+ function renderChip() {
413
+ const chip = S.vm && S.vm.chip;
414
+ if (!chipEl) return;
415
+ if (!chip || !Array.isArray(chip.segments) || !chip.segments.length) { chipEl.hidden = true; return; }
416
+ chipEl.hidden = false;
417
+ while (chipEl.firstChild) chipEl.removeChild(chipEl.firstChild);
418
+ const tone = ['up', 'down'].includes(chip.tone) ? chip.tone : '';
419
+ for (const sg of chip.segments.slice(0, 8)) {
420
+ const id = ['head', 'pnl', 'peak', 'px', 'held', 'dd', 'tp'].includes(sg.id) ? sg.id : 'x';
421
+ let cls = `seg ${id}`;
422
+ if (id === 'pnl') cls += ` ${tone}${chip.newPeak ? ' flash' : ''}`;
423
+ if (id === 'peak') cls += ` ${/^peak −/.test(String(sg.text)) ? 'down' : 'up'}`;
424
+ chipEl.appendChild(el('span', cls, String(sg.text).slice(0, 48)));
425
+ }
426
+ }
427
+
400
428
  // ---------------------------------------------------------------- incoming view models
401
429
  function onData(vm) {
402
430
  if (!vm || typeof vm !== 'object') return;
@@ -409,7 +437,7 @@
409
437
  // task-1366: a mode / supply change re-labels the axis in place (the bars are prices either way)
410
438
  const disp = C.displayOf(vm);
411
439
  if (disp.key !== S.display.key) { S.display = disp; applyPriceFormat(); }
412
- if (tail && (!vm.seriesKey || vm.seriesKey !== S.key)) { S.vm = { ...vm, candles: [] }; renderHeader(); renderPosition(); renderOverlays(); return; }
440
+ if (tail && (!vm.seriesKey || vm.seriesKey !== S.key)) { S.vm = { ...vm, candles: [] }; renderHeader(); renderPosition(); renderChip(); renderOverlays(); return; }
413
441
  const intervalChanged = vm.interval !== S.interval;
414
442
  S.interval = vm.interval;
415
443
  const answered = S.pendingInterval === vm.interval ? S.reqAt : null;
@@ -425,7 +453,7 @@
425
453
  if (intervalChanged || S.key !== prevKey) S.fitNext = true;
426
454
  renderFull();
427
455
  }
428
- renderHeader(); renderLegend(S.hover); renderDataBanner(); renderPosition(); syncToolbar(); renderLive();
456
+ renderHeader(); renderLegend(S.hover); renderDataBanner(); renderPosition(); renderChip(); syncToolbar(); renderLive();
429
457
  // rain mood follows direction (gold on a pump, red-shift on a dump), as in the v1 HUD; chg is in percent
430
458
  const dir = vm.chg ? (vm.chg.h24 ?? vm.chg.h1 ?? 0) : 0;
431
459
  if (rain) { rain.setMood(dir > 1 ? 'rainPump' : dir < -1 ? 'rainDump' : 'rain'); rain.setActivity(Math.min(1, 0.4 + Math.abs(dir) / 100)); rain.setSources([String(vm.symbol || ''), String(vm.mint || '').slice(0, 6)]); }
@@ -514,6 +542,7 @@
514
542
  const n = parseInt(k, 10);
515
543
  if (n >= 1 && n <= 6) { requestInterval(C.INTERVALS[n - 1]); return; }
516
544
  switch (k) {
545
+ case 'A': case 'y': setAxis(S.vm && S.vm.priceMode === 'mcap' ? 'price' : 'mcap'); break; // task-1472
517
546
  case 'c': setStyle(C.STYLES[(C.STYLES.indexOf(S.style) + 1) % C.STYLES.length], true); break;
518
547
  case 'a': setScale('auto', true); break;
519
548
  case 'l': setScale(S.scale === 'log' ? 'auto' : 'log', true); break;
@@ -43,3 +43,48 @@
43
43
  .spec-act-age { color: var(--warn); }
44
44
 
45
45
  .spec-foot-redact { color: var(--up); font-weight: 600; }
46
+
47
+ /* task-1466 spectator v2: the agent story (phase rail + heartbeat, thought ribbon, call timeline, vetting + PAPER cards) */
48
+ .spec-story { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; font: 12px ui-monospace, Menlo, monospace; }
49
+ .spec-rail { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
50
+ .spec-heart { width: 10px; height: 10px; border-radius: 50%; background: var(--down); margin-right: 6px; box-shadow: 0 0 8px var(--down); animation: spec-beat 1s infinite ease-out; }
51
+ .spec-heart.still { animation: none; background: var(--warn); box-shadow: none; }
52
+ .spec-heart.dead { animation: none; background: var(--dim); box-shadow: none; }
53
+ @keyframes spec-beat { 0% { transform: scale(1.35); opacity: 1; } 30% { transform: scale(0.9); opacity: 0.7; } 100% { transform: scale(1); opacity: 0.85; } }
54
+ .spec-bpm { color: var(--muted); margin-right: 8px; min-width: 44px; }
55
+ .spec-phase { padding: 2px 8px; border-radius: 4px; color: var(--dim); border: 1px solid transparent; letter-spacing: 0.05em; }
56
+ .spec-phase.done { color: var(--muted); }
57
+ .spec-phase.on { color: #000; background: var(--accent); font-weight: 700; box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 60%, transparent); }
58
+ .spec-phase.on.p-vetting { background: var(--borderFocus); box-shadow: 0 0 12px color-mix(in srgb, var(--borderFocus) 60%, transparent); }
59
+ .spec-phase.on.p-sizing { background: var(--warn); }
60
+ .spec-phase.on.p-executing { background: var(--up); }
61
+ .spec-phase.on.p-holding { background: var(--pump); }
62
+ .spec-phase.on.p-exiting { background: var(--ai, var(--accent)); }
63
+ .spec-sep { color: var(--border); }
64
+ .spec-paper { margin-left: auto; padding: 1px 6px; border-radius: 4px; background: var(--warn); color: #000; font-weight: 700; }
65
+ .spec-ctlstate { padding: 1px 6px; border-radius: 4px; font-weight: 700; color: #000; }
66
+ .spec-ctlstate.paused { background: var(--warn); }
67
+ .spec-ctlstate.takeover { background: var(--accent); }
68
+ .spec-ctlstate.killed { background: var(--down); color: #fff; }
69
+ .spec-ribbon { padding: 6px 8px; border-left: 3px solid var(--ai, var(--accent)); background: color-mix(in srgb, var(--panel) 85%, transparent); color: var(--text); min-height: 1.4em; }
70
+ .spec-ribbon.ours { border-left-color: var(--border); color: var(--muted); font-style: italic; }
71
+ .spec-ribbon-who { color: var(--ai, var(--accent)); margin-right: 6px; font-style: normal; }
72
+ .spec-timeline { display: flex; align-items: flex-start; gap: 0; overflow: hidden; justify-content: flex-end; }
73
+ .spec-node { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 2px; }
74
+ .spec-node-l { color: var(--text); white-space: nowrap; }
75
+ .spec-node.paper .spec-node-l { color: var(--warn); font-weight: 700; }
76
+ .spec-chip { font-size: 10px; padding: 0 5px; border-radius: 3px; background: color-mix(in srgb, var(--up) 18%, transparent); color: var(--up); }
77
+ .spec-chip.slow { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
78
+ .spec-chip.fail, .spec-chip.refused { background: color-mix(in srgb, var(--down) 22%, transparent); color: var(--down); }
79
+ .spec-link { color: var(--border); align-self: flex-start; margin-top: 1px; }
80
+ .spec-cards { display: flex; flex-wrap: wrap; gap: 8px; }
81
+ .spec-card { flex: 1 1 220px; border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; background: color-mix(in srgb, var(--panel) 85%, transparent); }
82
+ .spec-card-h { color: #000; background: var(--borderFocus); font-weight: 700; padding: 0 6px; border-radius: 3px; display: inline-block; margin-bottom: 4px; }
83
+ .spec-card.trade .spec-card-h { background: var(--pump); }
84
+ .spec-check { margin-right: 8px; color: var(--dim); }
85
+ .spec-check.ok { color: var(--up); font-weight: 600; }
86
+ .spec-big { font-size: 30px; font-weight: 800; letter-spacing: 0.02em; line-height: 1.1; }
87
+ .spec-big.up { color: var(--up); text-shadow: 0 0 14px color-mix(in srgb, var(--up) 45%, transparent); }
88
+ .spec-big.down { color: var(--down); }
89
+ .spec-kv { color: var(--muted); }
90
+ .spec-kv b { color: var(--text); font-weight: 600; }
@@ -12,6 +12,7 @@
12
12
  <div id="hud" class="hud">
13
13
  <div id="drag" class="drag"><span class="mark">✦</span><span id="title" class="title">dexterm · WATCH THE AGENT COOK</span><span id="status" class="status">connecting…</span><button id="close" class="close" type="button" aria-label="close">✕</button></div>
14
14
  <div class="stage"><canvas id="rain"></canvas><div class="body">
15
+ <div id="story" class="spec-story"></div>
15
16
  <div id="screen" class="spec-screen"></div>
16
17
  <div id="agents" class="spec-agents"></div>
17
18
  <div class="spec-activity-head">agent activity</div>
@@ -14,8 +14,77 @@
14
14
 
15
15
  let vm = null;
16
16
 
17
+ const pctS = (p) => (p == null ? '—' : `${p >= 0 ? '+' : '−'}${Math.abs(p * 100).toFixed(1)}%`);
18
+ const px = (n) => (n == null ? '—' : n >= 1 ? `$${n.toFixed(4)}` : `$${Number(n.toPrecision(3))}`);
19
+ const fmtMs = (ms) => (ms == null ? '·' : ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
20
+ const CHECKS = ['price', 'tape', 'chart', 'holders', 'rug', 'dev', 'larp', 'verdict'];
21
+
22
+ // task-1466 spectator v2: the agent story above the activity list (textContent only; strings redacted TUI-side).
23
+ function renderStory(st) {
24
+ const host = $('story'); host.textContent = '';
25
+ if (!st) return;
26
+ const rail = el('div', 'spec-rail');
27
+ const heart = el('span', 'spec-heart' + (st.control === 'killed' ? ' dead' : st.control !== 'running' ? ' still' : ''));
28
+ if (st.bpm > 0) heart.style.animationDuration = `${(60 / st.bpm).toFixed(2)}s`;
29
+ rail.appendChild(heart);
30
+ rail.appendChild(el('span', 'spec-bpm', st.control === 'killed' ? '— bpm' : `${st.bpm} bpm`));
31
+ const ai = st.rail.indexOf(st.phase);
32
+ st.rail.forEach((p, i) => {
33
+ if (i) rail.appendChild(el('span', 'spec-sep', '─'));
34
+ const on = i === ai && st.control !== 'killed';
35
+ rail.appendChild(el('span', 'spec-phase p-' + p + (on ? ' on' : i < ai ? ' done' : ''), (on ? '▸' : '') + p.toUpperCase() + (on && st.sub ? ` · ${st.sub}` : '')));
36
+ });
37
+ if (st.control !== 'running') rail.appendChild(el('span', 'spec-ctlstate ' + st.control, st.control === 'killed' ? '■ KILLED' : st.control === 'paused' ? '❚❚ PAUSED' : 'YOU ARE DRIVING'));
38
+ rail.appendChild(el('span', 'spec-paper', 'PAPER'));
39
+ host.appendChild(rail);
40
+ const th = (st.thoughts || []).slice().reverse();
41
+ const agentT = th.find((x) => x.source === 'agent');
42
+ const ours = th.find((x) => x.source === 'dexterm');
43
+ const pick = agentT && (!ours || agentT.t >= ours.t - 1500 || Date.now() - agentT.t < 9000) ? agentT : ours || agentT;
44
+ const rib = el('div', 'spec-ribbon' + (pick && pick.source === 'dexterm' ? ' ours' : ''));
45
+ if (pick) { if (pick.source === 'agent') rib.appendChild(el('span', 'spec-ribbon-who', '❯')); rib.appendChild(el('span', '', pick.text)); }
46
+ else rib.appendChild(el('span', 'spec-dim', 'the agent has not said anything yet'));
47
+ host.appendChild(rib);
48
+ const tl = el('div', 'spec-timeline');
49
+ (st.nodes || []).slice(-14).forEach((n, i) => {
50
+ if (i) tl.appendChild(el('span', 'spec-link', '──'));
51
+ const node = el('div', 'spec-node' + (n.paper ? ' paper' : ''));
52
+ node.appendChild(el('span', 'spec-node-l', `${n.status === 'refused' ? '✗' : n.paper ? '■' : '●'} ${n.label}${n.count > 1 ? ` ×${n.count}` : ''}`));
53
+ node.appendChild(el('span', 'spec-chip ' + n.status, n.status === 'refused' ? 'refused' : fmtMs(n.ms)));
54
+ tl.appendChild(node);
55
+ });
56
+ host.appendChild(tl);
57
+ const cards = el('div', 'spec-cards');
58
+ if (st.focus) {
59
+ const c = el('div', 'spec-card');
60
+ c.appendChild(el('span', 'spec-card-h', `VETTING $${st.focus.symbol}`));
61
+ const row = el('div');
62
+ for (const k of CHECKS) row.appendChild(el('span', 'spec-check' + (st.focus.checks.includes(k) ? ' ok' : ''), `${st.focus.checks.includes(k) ? '✓' : '·'}${k}`));
63
+ c.appendChild(row);
64
+ if (st.focus.larpFlags != null) c.appendChild(el('div', 'spec-kv', `LARP evidence: ${st.focus.larpFlags} flag${st.focus.larpFlags === 1 ? '' : 's'} (not a verdict)`));
65
+ if (st.focus.verdict) c.appendChild(el('div', 'spec-kv', `verdict ${st.focus.verdict}${st.focus.rug != null ? ` · rug heuristic ${Math.round(st.focus.rug * 100)}%` : ''}`));
66
+ cards.appendChild(c);
67
+ }
68
+ if (st.arc) {
69
+ const a = st.arc;
70
+ const c = el('div', 'spec-card trade');
71
+ c.appendChild(el('span', 'spec-card-h', `PAPER · $${a.symbol} · ${a.stage === 'closed' ? 'RESULT' : a.stage === 'open' ? 'HOLDING' : a.stage === 'refused' ? 'REFUSED' : 'TICKET'}`));
72
+ const big = a.stage === 'closed' ? a.resultPct : a.pnlPct;
73
+ if (big != null) c.appendChild(el('div', 'spec-big ' + (big >= 0 ? 'up' : 'down'), pctS(big)));
74
+ const kv = el('div', 'spec-kv');
75
+ kv.textContent = [a.sizeUsd != null ? `size $${a.sizeUsd.toFixed(2)}` : null, a.entryUsd != null ? `entry ${px(a.entryUsd)}` : null, a.markUsd != null && a.stage !== 'closed' ? `mark ${px(a.markUsd)}` : null,
76
+ a.peakPct != null ? `peak ${pctS(a.peakPct)}` : null, a.exits ? `exits ${a.exits}` : null, a.realizedSol != null ? `realized ${a.realizedSol >= 0 ? '+' : ''}${a.realizedSol.toFixed(4)} SOL` : null,
77
+ a.refusal ? `refused: ${a.refusal}` : null].filter(Boolean).join(' · ');
78
+ c.appendChild(kv);
79
+ c.appendChild(el('div', 'spec-dim', 'simulated paper trade · not a result'));
80
+ cards.appendChild(c);
81
+ }
82
+ if (cards.childNodes.length) host.appendChild(cards);
83
+ }
84
+
17
85
  function render() {
18
86
  if (!vm) return;
87
+ renderStory(vm.story || null);
19
88
 
20
89
  const scr = $('screen'); scr.textContent = '';
21
90
  const s = vm.screen || {};
@@ -28,4 +28,6 @@ export const CHART_INDICATORS = ['ema9', 'ema21', 'vwap', 'volma', 'volume', 'ta
28
28
  export const CHART_STYLES = ['candles', 'line', 'area'];
29
29
  /** Price-scale mode: auto (normal, autoscaled), logarithmic, percent (from the first visible bar). */
30
30
  export const CHART_SCALES = ['auto', 'log', 'percent'];
31
+ /** task-1472: what the chart's Y axis shows (the popup's MCAP | PRICE control / `A` ask the terminal to switch). */
32
+ export const CHART_AXES = ['mcap', 'price'];
31
33
  export const PROTOCOL_VERSION = 'dexterm-popup/1';
@@ -26,7 +26,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
26
26
  import { readFileSync, existsSync } from 'node:fs';
27
27
  import { fileURLToPath } from 'node:url';
28
28
  import path from 'node:path';
29
- import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
29
+ import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, CHART_AXES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
30
30
  import { auditWidgetHtml, inlineHashes, normalizeWidgetHtml } from "./widget-html.js";
31
31
  const here = path.dirname(fileURLToPath(import.meta.url));
32
32
  const PAGES_DIR = path.join(here, 'pages');
@@ -48,6 +48,8 @@ export const POPUP_EVENTS = new Set([
48
48
  'event:popup.close-request', 'event:popup.focus', 'event:popup.drag',
49
49
  // task-1325 chart v2 requests (payloads validated by validateEventPayload below)
50
50
  'event:chart.interval', 'event:chart.indicator', 'event:chart.style', 'event:chart.scale', 'event:chart.painted',
51
+ // task-1472: the chart's Y axis (market cap ⇄ price): a display preference the terminal saves, nothing else
52
+ 'event:chart.axis',
51
53
  // task-1345 agent roster: per-agent pause / kick, by agent name (the only writes this popup can send)
52
54
  'event:roster.pause', 'event:roster.resume', 'event:roster.kick',
53
55
  // task-1355 order plans: cancel a plan by id (the ONLY write the plans popup can send — never arm/cap/sign)
@@ -94,6 +96,8 @@ export function validateEventPayload(name, payload) {
94
96
  return oneOf(CHART_STYLES, p.style) ? { style: p.style } : null;
95
97
  case 'chart.scale':
96
98
  return oneOf(CHART_SCALES, p.scale) ? { scale: p.scale } : null;
99
+ case 'chart.axis':
100
+ return oneOf(CHART_AXES, p.axis) ? { axis: p.axis } : null;
97
101
  case 'chart.painted':
98
102
  return (p.kind === 'first' || p.kind === 'data') && finiteIn(p.ms, 0, 600_000) && finiteIn(p.sentAt, 0, 1e15)
99
103
  ? { kind: p.kind, ms: p.ms, sentAt: p.sentAt } : null;
@@ -38,7 +38,7 @@ export function queryDevHistory(db, q) {
38
38
  creator: q.creator, status: 'unknown', asOf, source: DEV_HISTORY_SOURCE,
39
39
  launches: { n: 0, newestT: null, ageSec: null },
40
40
  labelled: { n: 0, rug: 0, dead: 0, fade: 0, flat: 0, runner: 0, horizon: '72h preferred', tier: 'keyless-proxy', newestT: null, ageSec: null },
41
- rugRate: null, badRate: null, devExit: { n: 0, medianMin: null, within60m: 0 }, serialRugger: false, recent: [],
41
+ rugRate: null, badRate: null, devExit: { n: 0, medianMin: null, within60m: 0, newestT: null, ageSec: null }, serialRugger: false, recent: [],
42
42
  };
43
43
  let links;
44
44
  try {
@@ -56,6 +56,7 @@ export function queryDevHistory(db, q) {
56
56
  const counts = { rug: 0, dead: 0, fade: 0, flat: 0, runner: 0 };
57
57
  let labelledN = 0, labelledNewest = null;
58
58
  const exits = [];
59
+ let exitNewest = null;
59
60
  const launches = [];
60
61
  for (const l of links) {
61
62
  const firstT = Number(l.first_t);
@@ -93,6 +94,7 @@ export function queryDevHistory(db, q) {
93
94
  const from = createdAt ?? Number(series[held].t);
94
95
  devExitMin = Math.max(0, (Number(out.t) - from) / 60_000);
95
96
  exits.push(devExitMin);
97
+ exitNewest = Math.max(exitNewest ?? 0, Number(out.t));
96
98
  }
97
99
  }
98
100
  }
@@ -109,12 +111,14 @@ export function queryDevHistory(db, q) {
109
111
  labelled: { ...empty.labelled, n: labelledN, ...counts, newestT: labelledNewest, ageSec: labelledNewest === null ? null : Math.max(0, Math.round((asOf - labelledNewest) / 1000)) },
110
112
  rugRate: labelledN ? counts.rug / labelledN : null,
111
113
  badRate: labelledN ? (counts.rug + counts.dead) / labelledN : null,
112
- devExit: { n: exits.length, medianMin: median(exits), within60m: exits.filter((m) => m <= 60).length },
114
+ devExit: { n: exits.length, medianMin: median(exits), within60m: exits.filter((m) => m <= 60).length, newestT: exitNewest, ageSec: exitNewest === null ? null : Math.max(0, Math.round((asOf - exitNewest) / 1000)) },
113
115
  serialRugger: counts.rug >= SERIAL_RUGGER_MIN,
114
116
  recent: launches,
115
117
  };
116
118
  }
117
- const ago = (sec) => sec === null ? 'age unknown' : sec < 3600 ? `${Math.max(1, Math.round(sec / 60))}m ago` : sec < 172800 ? `${Math.round(sec / 3600)}h ago` : `${Math.round(sec / 86400)}d ago`;
119
+ /** Compact age for a panel cell: `5m`, `3h`, `40d`; `age ?` when unknown. */
120
+ export const ageShort = (sec) => sec === null ? 'age ?' : sec < 3600 ? `${Math.max(1, Math.round(sec / 60))}m` : sec < 172800 ? `${Math.round(sec / 3600)}h` : `${Math.round(sec / 86400)}d`;
121
+ const ago = (sec) => sec === null ? 'age unknown' : `${ageShort(sec)} ago`;
118
122
  /**
119
123
  * The one-line summary every surface shows (DEV panel, rug-signal row, MCP note): counts with the sample size and
120
124
  * the age, or `unknown` with why. Plain wording: a risk flag from observed history, never "rug confirmed" or "safe".
@@ -66,6 +66,11 @@ export class Canvas {
66
66
  */
67
67
  images;
68
68
  imageMode;
69
+ /**
70
+ * task-1466: where each keyed list row was drawn (a table with `rowKey`, e.g. a screener row by mint), so an
71
+ * overlay drawn later (the agent HUD's row glow / cursor) can find the row on screen. Not emitted anywhere.
72
+ */
73
+ rowMarks;
69
74
  constructor(w, h, theme) {
70
75
  this.w = w;
71
76
  this.h = h;
@@ -82,6 +87,7 @@ export class Canvas {
82
87
  this.shields = [];
83
88
  this.images = [];
84
89
  this.imageMode = 'cells';
90
+ this.rowMarks = [];
85
91
  }
86
92
  idx(x, y) { return y * this.w + x; }
87
93
  /** Copy every buffer of `src` (same size) into this canvas. */
@@ -97,6 +103,7 @@ export class Canvas {
97
103
  this.shields = src.shields.slice();
98
104
  this.images = src.images.slice();
99
105
  this.imageMode = src.imageMode;
106
+ this.rowMarks = src.rowMarks.slice();
100
107
  }
101
108
  put(x, y, ch, st = {}) {
102
109
  if (x < 0 || y < 0 || x >= this.w || y >= this.h)
@@ -119,6 +119,59 @@ export function fmtMcap(n) {
119
119
  return bare;
120
120
  return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
121
121
  }
122
+ /**
123
+ * task-1472: THE chart number formatter (Y axis, crosshair/legend, last-value tag, volume): compact with a LOWERCASE k
124
+ * for thousands, then M, B, T. One decimal while the value is below 10 of its unit, none after, trailing `.0` dropped:
125
+ * 4930 → 4.9k, 42100 → 42k, 487000 → 487k, 1.23e6 → 1.2M, 4.8e7 → 48M, 1.3e9 → 1.3B, 999 → 999. The unit is promoted
126
+ * AFTER rounding, so 999 900 reads 1M (never 1000k). Below 1 it is the subscript price form (0.0000123 → 0.0₄123):
127
+ * never scientific notation, never a long run of zeros. `extra` (0..3) adds decimals when an axis needs them to keep
128
+ * two neighbouring ticks apart (see compactDecimalsFor). No currency symbol; fmtCompactUsd adds the `$`.
129
+ */
130
+ export function fmtCompact(n, extra = 0) {
131
+ if (n == null || !Number.isFinite(n))
132
+ return '—';
133
+ if (n === 0)
134
+ return '0';
135
+ const sign = n < 0 ? '-' : '';
136
+ const a = Math.abs(n);
137
+ if (a < 1)
138
+ return sign + trimZeros(fmtPriceBare(a));
139
+ const units = ['', 'k', 'M', 'B', 'T'];
140
+ const x = Math.max(0, Math.min(3, Math.floor(extra)));
141
+ const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
142
+ let u = 0, v = a;
143
+ while (u < units.length - 1 && v >= 1000) {
144
+ v /= 1000;
145
+ u++;
146
+ }
147
+ let r = round(v);
148
+ if (r >= 1000 && u < units.length - 1) {
149
+ u++;
150
+ r = round(r / 1000);
151
+ }
152
+ return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${units[u]}`;
153
+ }
154
+ /** USD form of fmtCompact: `$4.9k`, `$48M`, `-$1.2k`. */
155
+ export function fmtCompactUsd(n, extra = 0) {
156
+ const bare = fmtCompact(n, extra);
157
+ if (bare === '—')
158
+ return bare;
159
+ return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
160
+ }
161
+ /**
162
+ * task-1472: the extra decimals (0..3) fmtCompact needs so that axis ticks `step` apart around `around` never print
163
+ * the same label (a 48.1M–48.6M range would otherwise read 48M on every tick). 0 for a normal memecoin range.
164
+ */
165
+ export function compactDecimalsFor(around, step) {
166
+ if (!Number.isFinite(around) || !Number.isFinite(step) || step <= 0 || Math.abs(around) < 1)
167
+ return 0;
168
+ for (let x = 0; x < 3; x++) {
169
+ const a = fmtCompact(around, x);
170
+ if (a !== fmtCompact(around + step, x) && a !== fmtCompact(around - step, x))
171
+ return x;
172
+ }
173
+ return 3;
174
+ }
122
175
  export function fmtPct(n, { sign = true, digits = 1 } = {}) {
123
176
  if (n == null || Number.isNaN(n))
124
177
  return '—';
@@ -268,6 +268,10 @@ export const DEFAULT_KEYMAP = new Keymap([
268
268
  // the open coin. (`P` is already the global positions key, so chart uses `C`.) Collision-free with the
269
269
  // token screen's other capitals (B/S trade, W/X/T/L links, V vet).
270
270
  { keys: ['C'], action: 'popup.chart', label: 'chart popup', screen: 'token' },
271
+ // task-1472: `A` (axis) on the token screen flips the chart Y axis MARKET CAP ⇄ PRICE (both charts; saved as
272
+ // display.chartAxis). Free capital on the token screen (A is only auto-mirror on the follows screen); deliberately
273
+ // not Y, which confirms inside the PAPER ticket and the modals. The chart popup uses the same `A`.
274
+ { keys: ['A'], action: 'display.chartAxis', label: 'chart axis mcap ⇄ price', screen: 'token' },
271
275
  // task-1359: `I` on the token screen opens/toggles the TOKEN-DETAIL HUD (identity, market, risk, snipers/bundled
272
276
  // from the wallet-intel scan); `M` on the trending screen opens/toggles the FEEDS HUD (the keyless social feed +
273
277
  // official news, newest first; ⏎ in the window opens that coin here). Both are read-only windows. `I` is unused
@@ -5,7 +5,7 @@
5
5
  // background (hard cells); glow is only ever applied to neighbouring blank cells or to bg tints.
6
6
  import { Canvas } from "./canvas.js";
7
7
  import { mix, luminance } from "./color.js";
8
- import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
8
+ import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtPriceBare, fmtCompactUsd, compactDecimalsFor, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
9
9
  import { launchpadColor } from "./theme.js";
10
10
  export const BLOCKS = '▁▂▃▄▅▆▇█';
11
11
  // braille dot bits: column 0 rows 0..3, column 1 rows 0..3 (row 0 = top)
@@ -107,6 +107,7 @@ export function brailleLine(c, x, y, w, h, values, opts = {}) {
107
107
  c.put(x + col, y + r, String.fromCharCode(0x2800 + bits), { fg: neon(color, col / (w - 1), bg), bg, soft: true });
108
108
  }
109
109
  }
110
+ /** task-1472: the price axis never prints scientific notation: subscript zeros below 0.01 (0.0₅493), like fmtPrice. */
110
111
  function fmtPriceAxis(p) {
111
112
  if (p >= 1000)
112
113
  return p.toFixed(0);
@@ -114,7 +115,7 @@ function fmtPriceAxis(p) {
114
115
  return p.toFixed(3);
115
116
  if (p >= 0.001)
116
117
  return p.toFixed(5);
117
- return p.toExponential(2);
118
+ return fmtPriceBare(p);
118
119
  }
119
120
  /**
120
121
  * Candlestick chart with right-hand price axis, last-price tag, volume strip and time axis.
@@ -137,7 +138,6 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
137
138
  // task-1366: the bars are laid out in price units either way (identical glyphs); only the labels are scaled
138
139
  const s = opts.scale && Number.isFinite(opts.scale) && opts.scale > 0 ? opts.scale : 1;
139
140
  const baseFmt = opts.axisFmt ?? fmtPriceAxis;
140
- const fmtAxis = s === 1 ? baseFmt : (p) => baseFmt(p * s);
141
141
  let lo = Infinity, hi = -Infinity, vmax = 0;
142
142
  for (const k of data) {
143
143
  lo = Math.min(lo, k.l);
@@ -152,6 +152,9 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
152
152
  const gridStyle = { fg: mix(bg, t.border, 0.9), bg, soft: true };
153
153
  const labelStyle = { fg: t.muted, bg };
154
154
  const ticks = Math.max(2, Math.min(6, Math.floor(plotH / 3)));
155
+ // task-1472: one more decimal only when two neighbouring ticks would otherwise print the same compact label
156
+ const extra = opts.axisFmt ? compactDecimalsFor(((hi + lo) / 2) * s, (range / ticks) * s) : 0;
157
+ const fmtAxis = (p) => baseFmt(p * s, extra);
155
158
  for (let i = 0; i <= ticks; i++) {
156
159
  const row = Math.round((i / ticks) * (plotH - 1));
157
160
  const price = hi - (row / (plotH - 1)) * range;
@@ -245,7 +248,7 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
245
248
  }
246
249
  }
247
250
  c.text(x + plotW + 1, vy, 'vol', labelStyle);
248
- c.text(x + plotW + 1, vy + 1, fmtUsd(last.v, { digits: 1 }), { fg: t.text, bg });
251
+ c.text(x + plotW + 1, vy + 1, fmtCompactUsd(last.v), { fg: t.text, bg }); // task-1472: volume stays USD, compact k/M
249
252
  }
250
253
  const ty = y + h - 1;
251
254
  const labels = Math.max(2, Math.floor(plotW / 18));
@@ -386,6 +389,8 @@ export function table(c, x, y, w, h, spec) {
386
389
  const selected = ri === spec.selected;
387
390
  const rowBg = selected ? t.selBg : (r % 2 === 1 && spec.zebra !== false ? mix(bg, t.panelAlt, 1) : bg);
388
391
  c.fill(x, ry, w, 1, ' ', { bg: rowBg });
392
+ if (spec.rowKey)
393
+ c.rowMarks.push({ key: spec.rowKey(row), x, y: ry, w });
389
394
  if (selected)
390
395
  c.put(x - 1 >= 0 ? x - 1 : x, ry, '▎', { fg: t.accent, bg: rowBg, soft: true });
391
396
  cx = x;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dexterm-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "mcpName": "io.github.dexterm-xyz/dexterm",
5
5
  "description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
6
6
  "author": "The dexterm authors",