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
@@ -14,11 +14,52 @@
14
14
  // kill" are local follow controls: pause/take-over stop the terminal mirroring the agent (the external agent
15
15
  // keeps running — we simply stop moving the screen), and kill removes the rendezvous so the server stops
16
16
  // emitting. Any user keypress during an agent move takes control, so the human and the agent never fight.
17
- import { writeFileSync, readFileSync, rmSync, mkdirSync, statSync } from 'node:fs';
17
+ import { writeFileSync, readFileSync, rmSync, mkdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
18
18
  import { dirname } from 'node:path';
19
19
  import { randomBytes } from 'node:crypto';
20
- import { SpectatorHub, FollowController, SpectatorServer, } from '../../agents/src/index.js';
20
+ import { SpectatorHub, FollowController, SpectatorServer, AgentStory, sanitizeDetail, } from '../../agents/src/index.js';
21
21
  import { rendezvousPath, writeFollowScreen, clearFollowScreen } from '../../mcp/src/index.js';
22
+ /**
23
+ * task-1466: the agent's own words in one JSONL line of a thoughts feed. Understands Claude Code stream-json
24
+ * (`assistant` messages: text and thinking blocks), the capture harness's brain feed (`{tEpoch, ev}` wrapping one),
25
+ * and plain `{"thought": "…"}` / `{"text": "…"}` lines. Anything else yields nothing. Text only; never acted on.
26
+ */
27
+ export function thoughtsOf(line) {
28
+ const s = line.trim();
29
+ if (!s || s[0] !== '{' || s.length > 200_000)
30
+ return [];
31
+ let o;
32
+ try {
33
+ o = JSON.parse(s);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ let t = typeof o.tEpoch === 'number' && Number.isFinite(o.tEpoch) ? o.tEpoch : null;
39
+ if (o.ev && typeof o.ev === 'object')
40
+ o = o.ev;
41
+ if (t === null && typeof o.t === 'number' && Number.isFinite(o.t) && o.t > 1e12)
42
+ t = o.t;
43
+ // task-1473: a demo / stand-in feed marks its scripted lines `"demo": true` (the brain view tags them "demo text")
44
+ if (typeof o.thought === 'string')
45
+ return [{ text: o.thought, t, ...(o.demo === true ? { demo: true } : {}) }];
46
+ if (o.type === 'assistant' && o.message && typeof o.message === 'object') {
47
+ const content = o.message.content;
48
+ if (!Array.isArray(content))
49
+ return [];
50
+ const out = [];
51
+ for (const b of content) {
52
+ if (b?.type === 'text' && typeof b.text === 'string')
53
+ out.push({ text: b.text, t });
54
+ else if (b?.type === 'thinking' && typeof b.thinking === 'string' && b.thinking.trim())
55
+ out.push({ text: b.thinking, t });
56
+ }
57
+ return out;
58
+ }
59
+ if (typeof o.text === 'string' && !o.type)
60
+ return [{ text: o.text, t }];
61
+ return [];
62
+ }
22
63
  function navLabel(agent, nav) {
23
64
  const coin = nav.mint ? ` ${nav.mint.length > 10 ? nav.mint.slice(0, 4) + '…' + nav.mint.slice(-4) : nav.mint}` : '';
24
65
  if (nav.screen === 'token')
@@ -45,6 +86,8 @@ function navLabel(agent, nav) {
45
86
  export class McpFollowSession {
46
87
  hub;
47
88
  follow;
89
+ /** task-1466: the story the spectator v2 HUD draws (phase rail, timeline, thoughts, PAPER trade arc). */
90
+ story;
48
91
  opts;
49
92
  rvPath;
50
93
  channelPath;
@@ -52,6 +95,7 @@ export class McpFollowSession {
52
95
  agentLabel;
53
96
  currentAgent;
54
97
  offset = 0;
98
+ thoughtsOffset = 0;
55
99
  timer = null;
56
100
  stream = null;
57
101
  armed = false;
@@ -71,7 +115,8 @@ export class McpFollowSession {
71
115
  kickAgent: () => { this.disarm(); return 1; },
72
116
  };
73
117
  this.follow = new FollowController({ controls, activityCap: Math.max(1, opts.activityCap ?? 200) });
74
- this.hub.on((ev) => this.follow.apply(ev));
118
+ this.story = new AgentStory(this.agentLabel);
119
+ this.hub.on((ev) => { this.follow.apply(ev); this.story.apply(ev); });
75
120
  }
76
121
  /** Arm the rendezvous (so a `dexterm mcp` server starts emitting) and begin tailing. Idempotent. */
77
122
  async start() {
@@ -133,6 +178,51 @@ export class McpFollowSession {
133
178
  }
134
179
  /** Read and fold any records appended since the last pump. Public so tests can drive it without timers. */
135
180
  pump() {
181
+ this.pumpThoughts();
182
+ this.pumpChannel();
183
+ }
184
+ /** task-1466: new lines of the agent's own words → the story's thought ribbon (bounded: ≤ 256 KB per pump). */
185
+ pumpThoughts() {
186
+ const file = this.opts.thoughtsFile;
187
+ if (!file)
188
+ return;
189
+ let text;
190
+ try {
191
+ const st = statSync(file);
192
+ if (st.size < this.thoughtsOffset)
193
+ this.thoughtsOffset = 0;
194
+ if (st.size === this.thoughtsOffset)
195
+ return;
196
+ if (st.size - this.thoughtsOffset > 256 * 1024)
197
+ this.thoughtsOffset = st.size - 256 * 1024; // skip a backlog, keep the tail
198
+ const fd = openSync(file, 'r');
199
+ try {
200
+ const buf = Buffer.alloc(st.size - this.thoughtsOffset);
201
+ readSync(fd, buf, 0, buf.length, this.thoughtsOffset);
202
+ text = buf.toString('utf8');
203
+ }
204
+ finally {
205
+ closeSync(fd);
206
+ }
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ const nl = text.lastIndexOf('\n');
212
+ if (nl < 0)
213
+ return;
214
+ this.thoughtsOffset += Buffer.byteLength(text.slice(0, nl + 1), 'utf8');
215
+ const now = (this.opts.now ?? Date.now)();
216
+ for (const line of text.slice(0, nl).split('\n')) {
217
+ for (const th of thoughtsOf(line))
218
+ this.story.thought(th.text, th.t ?? now, 'agent', th.demo === true);
219
+ }
220
+ try {
221
+ this.opts.onEvent?.();
222
+ }
223
+ catch { /* ignore */ }
224
+ }
225
+ pumpChannel() {
136
226
  let text;
137
227
  try {
138
228
  const st = statSync(this.channelPath);
@@ -184,6 +274,7 @@ export class McpFollowSession {
184
274
  ms: rec.ms ?? null,
185
275
  dataAgeSec: rec.dataAgeSec ?? null,
186
276
  nav: rec.nav ?? null,
277
+ detail: sanitizeDetail(rec.detail),
187
278
  ok: rec.ok ?? true,
188
279
  t: typeof rec.t === 'number' ? rec.t : undefined,
189
280
  });
@@ -225,10 +316,14 @@ export class McpFollowSession {
225
316
  takeOver() { const l = this.follow.takeOver(this.currentAgent); this.paused = false; this.control('takeover'); this.opts.onStatus?.(l); return l; }
226
317
  resume() { const l = this.follow.resume(this.currentAgent); this.paused = false; this.control('running'); this.opts.onStatus?.(l); return l; }
227
318
  kill() { const l = this.follow.kill(this.currentAgent); this.paused = false; this.control('stopped'); this.opts.onStatus?.(l); return l; }
228
- control(state) { try {
229
- this.opts.onControl?.(state);
319
+ control(state) {
320
+ const now = (this.opts.now ?? Date.now)();
321
+ this.story.setControl(state === 'stopped' ? 'killed' : state, now);
322
+ try {
323
+ this.opts.onControl?.(state);
324
+ }
325
+ catch { /* never breaks the key loop */ }
230
326
  }
231
- catch { /* never breaks the key loop */ } }
232
327
  /** Any physical keypress while the agent is driving: the human takes over (no fight over the screen). */
233
328
  userKey() { if (this.follow.controlState === 'agent')
234
329
  this.takeOver(); }
@@ -237,6 +332,8 @@ export class McpFollowSession {
237
332
  get screenModel() { return this.follow.screenModel; }
238
333
  get streamPort() { return this.stream?.port ?? 0; }
239
334
  view() { return this.follow.view(); }
335
+ /** task-1466: the story at `now` for the HUD. */
336
+ storyView(now = (this.opts.now ?? Date.now)()) { return this.story.view(now); }
240
337
  /** Tear down: stop the timer, remove the rendezvous, close the stream. Safe to call more than once. */
241
338
  async stop() {
242
339
  if (this.timer) {
@@ -9,6 +9,7 @@
9
9
  // dexterm plan journal [--verify] [--export]
10
10
  // dexterm mandate draft [--total 100] [--per-trade 20] [--per-coin 40] [--daily-loss 50]
11
11
  // [--max-tier keypress|ptt-held|hands-free] [--ttl-hours 4]
12
+ // [--max-open 20] [--max-impact 3] [--paper]
12
13
  // dexterm mandate list [--json] | arm <id> | revoke <id>
13
14
  //
14
15
  // Arming is the only gate: `arm` reads ONE real keystroke in raw mode (ttyKeypress) and refuses a pipe,
@@ -44,7 +45,10 @@ const PLAN_USAGE = `dexterm plan <command>
44
45
  journal [--verify] [--export]`;
45
46
  const MANDATE_USAGE = `dexterm mandate <command>
46
47
  draft [--total 100] [--per-trade 20] [--per-coin 40] [--daily-loss 50]
47
- [--max-tier keypress|ptt-held|hands-free] [--ttl-hours 4] [--json]
48
+ [--max-tier keypress|ptt-held|hands-free] [--ttl-hours 4] [--max-open 20] [--max-impact 3]
49
+ [--paper] [--json]
50
+ (--paper: a PAPER-ONLY mandate for the agent paper desk, with its own
51
+ paper ceilings; no plan, copy trade or signer can ever use it)
48
52
  list [--json]
49
53
  arm <id> (needs a physical keypress on a TTY; a pipe/agent is refused)
50
54
  revoke <id>`;
@@ -203,16 +207,20 @@ export async function runMandate(rest, io) {
203
207
  const num = (n) => (flag(rest, n) !== undefined ? parseAmount(flag(rest, n)) : undefined);
204
208
  const maxTier = flag(rest, '--max-tier');
205
209
  const ttlHours = flag(rest, '--ttl-hours') ? Number(flag(rest, '--ttl-hours')) : undefined;
210
+ const plain = (n) => (flag(rest, n) !== undefined ? Number(flag(rest, n)) : undefined);
206
211
  const m = svc.draftMandate({
207
212
  totalUsd: num('--total'), perTradeUsd: num('--per-trade'), perCoinUsd: num('--per-coin'),
208
213
  dailyLossUsd: num('--daily-loss'), maxTier, ttlMs: ttlHours ? ttlHours * 3600_000 : undefined,
214
+ maxOpenPositions: plain('--max-open'), maxPriceImpactPct: plain('--max-impact'),
215
+ ...(has(rest, '--paper') ? { paperOnly: true } : {}),
209
216
  });
210
217
  if (has(rest, '--json')) {
211
- out(JSON.stringify({ id: m.id, budget: m.budget, maxTier: m.maxTier }));
218
+ out(JSON.stringify({ id: m.id, budget: m.budget, maxTier: m.maxTier, paperOnly: m.paperOnly === true }));
212
219
  return 0;
213
220
  }
214
- out(`drafted ${m.id}`);
221
+ out(`drafted ${m.id}${m.paperOnly ? ' (PAPER-ONLY: agent paper desk only)' : ''}`);
215
222
  out(` total ${fmtUsd(m.budget.totalUsd)} · per-trade ${fmtUsd(m.budget.perTradeUsd)} · per-coin ${fmtUsd(m.budget.perCoinUsd)} · daily-loss ${fmtUsd(m.budget.dailyLossUsd)} · maxTier ${m.maxTier}`);
223
+ out(` max open ${m.limits.maxOpenPositions} · max price impact ${m.limits.maxPriceImpactPct}%`);
216
224
  out(' DRAFT only — not armed. Arm with: dexterm mandate arm ' + m.id + ' (physical keypress)');
217
225
  return 0;
218
226
  }
@@ -228,7 +236,7 @@ export async function runMandate(rest, io) {
228
236
  return 0;
229
237
  }
230
238
  for (const r of rows)
231
- out(`${r.id.padEnd(24)} ${r.status.padEnd(7)} total ${fmtUsd(r.totalUsd)} · reserved ${fmtUsd(r.reservedUsd)} · spent ${fmtUsd(r.spentUsd)} · maxTier ${r.maxTier}`);
239
+ out(`${r.id.padEnd(24)} ${r.status.padEnd(7)} total ${fmtUsd(r.totalUsd)} · reserved ${fmtUsd(r.reservedUsd)} · spent ${fmtUsd(r.spentUsd)} · maxTier ${r.maxTier}${r.paperOnly ? ' · PAPER-ONLY' : ''}`);
232
240
  return 0;
233
241
  }
234
242
  case 'arm': {
@@ -243,7 +251,9 @@ export async function runMandate(rest, io) {
243
251
  errOut(`no such mandate ${id}`);
244
252
  return 1;
245
253
  }
246
- out(`Arm standing mandate ${id}? This lets armed plans fire at machine speed from the capped fast wallet.`);
254
+ out(row.paperOnly
255
+ ? `Arm PAPER-ONLY mandate ${id}? This lets the agent paper desk book simulated fills under these caps. No plan, copy trade or signer can use it.`
256
+ : `Arm standing mandate ${id}? This lets armed plans fire at machine speed from the capped fast wallet.`);
247
257
  out(` total ${fmtUsd(row.totalUsd)} · per-trade ${fmtUsd(row.perTradeUsd)} · per-coin ${fmtUsd(row.perCoinUsd)} · maxTier ${row.maxTier}`);
248
258
  out('Hold any key to confirm (physical keypress). A pipe or agent cannot arm. Waiting 15s…');
249
259
  const key = await ttyKeypress(io.stdin, 15_000);
@@ -14,6 +14,7 @@ import { fmtPrice, fmtNum } from '../../tui/src/index.js';
14
14
  import { positionRow } from "./live/trade.js";
15
15
  import { signedUsd, signedPctOf, fmtUsdFine, sol as fmtSol, signedSol } from "./screens/positions.js";
16
16
  import { supplyRatio, positionPriceCells } from "./display.js";
17
+ import { chipSegments } from "./screens/pnl-chip.js";
17
18
  /** Map the app's active Theme to the popup protocol's theme-token subset. */
18
19
  export function popupThemeFrom(t) {
19
20
  return {
@@ -160,6 +161,12 @@ export function tokenDetailVMFromView(v, intel = null) {
160
161
  flags: v.flags.map((f) => String(f)), holdersSample: v.holdersSample, updatedAt: v.updatedAt,
161
162
  };
162
163
  }
164
+ /** task-1473 r2: the chip as the chart page draws it (same segments as the terminal). */
165
+ export function chartChipFrom(m) {
166
+ if (!m)
167
+ return null;
168
+ return { kind: m.kind, symbol: m.symbol, tone: m.pnlPct === null ? 'muted' : m.pnlPct >= 0 ? 'up' : 'down', newPeak: m.newPeak, segments: chipSegments(m) };
169
+ }
163
170
  /** "docked right of Terminal" / "screen right (unknown terminal)" — for the TUI toast. */
164
171
  export function describeDock(p) {
165
172
  if (!p)
@@ -432,6 +439,14 @@ export class TokenPopups {
432
439
  case 'chart.scale':
433
440
  this.prefs = { ...this.prefs, scale: p.scale };
434
441
  return;
442
+ case 'chart.axis': { // task-1472: a display preference the terminal saves; nothing else
443
+ if (p.axis !== 'mcap' && p.axis !== 'price')
444
+ return;
445
+ this.source?.setChartAxis?.(p.axis);
446
+ if (this.lastView)
447
+ this.push(this.lastView);
448
+ return;
449
+ }
435
450
  case 'chart.painted': {
436
451
  const ms = Number(p.ms);
437
452
  if (p.kind === 'first' && this.metrics.openedAt !== null && this.metrics.openToFirstPaintMs === null)
@@ -520,8 +535,9 @@ export class TokenPopups {
520
535
  prefs: this.prefs,
521
536
  now: this.now(),
522
537
  image: src.imagePath?.(view) ?? null,
523
- priceMode: src.priceMode?.() ?? 'price',
538
+ priceMode: src.chartAxis?.() ?? src.priceMode?.() ?? 'price', // task-1472: the chart axis drives the popup
524
539
  });
540
+ vm.chip = chartChipFrom(src.pnlChip?.() ?? null); // task-1473 r2
525
541
  const key = vm.seriesKey ?? null;
526
542
  const n = vm.candles.length;
527
543
  const canTail = !full && this.ready && key !== null && key === this.lastKey && this.lastLen > 0 && n >= this.lastLen && n <= this.lastLen + 1;
@@ -11,8 +11,14 @@ export const SCAN_TICK_MS = 2_000;
11
11
  export const SCAN_RECOMPUTE_MS = 10_000;
12
12
  /** The analyst UI actions a scan session may request: NONE (observe-only). */
13
13
  export const SCAN_ACTION_TYPES = Object.freeze([]);
14
+ /** task-1473: append one line to the scan's session log (the analyst panel's brain view shows it). */
15
+ export function scanLog(scan, t, kind, symbol, text) {
16
+ scan.log.push({ t, kind, symbol: cleanText(symbol, 12), text });
17
+ if (scan.log.length > 40)
18
+ scan.log.splice(0, scan.log.length - 40);
19
+ }
14
20
  export function makeScanUi(cfg = {}) {
15
- return { engine: new ScanEngine(cfg), sig: '', lastTickAt: 0, lastComputeAt: 0, session: null, blocked: [], returnTo: null };
21
+ return { engine: new ScanEngine(cfg), sig: '', lastTickAt: 0, lastComputeAt: 0, session: null, blocked: [], returnTo: null, log: [] };
16
22
  }
17
23
  export const MODE_LABEL = { off: 'OFF', scanning: 'SCANNING', locked: 'LOCKED', paused: 'PAUSED' };
18
24
  const SHORT = { vol: 'vol', flow: 'flow', fresh: 'fresh', breakout: 'brk', liq: 'liq', holders: 'hold' };