dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// dexterm — full-screen Analyst (chat with screener-aware Claude) + live context rail.
|
|
2
|
+
// Cyberspace styling (task-1297): Claude's verdict/answer lines and tool-call lines in coral, the
|
|
3
|
+
// streaming answer decrypts through code glyphs behind the Claude-Code spinner, rain zones under the
|
|
4
|
+
// transcript and the market pulse, ▲/▼ on every direction.
|
|
5
|
+
import { Canvas, sparkline, heatPct, decryptText, spinnerGlyph, fmtUsd, fmtPrice, fmtPctDir, fmtDeltaDir, wrap, header, footer, breakpoint, mix, isCalm, } from '../../../tui/src/index.js';
|
|
6
|
+
import { headerModel } from "../state.js";
|
|
7
|
+
import { screenerView } from "../screener/view.js";
|
|
8
|
+
import { renderAnalystSession } from "./analyst-session.js";
|
|
9
|
+
import { resolveAnalystResults } from "../analyst/results.js";
|
|
10
|
+
export function renderAnalyst(c, state) {
|
|
11
|
+
const t = c.theme;
|
|
12
|
+
const bp = breakpoint(c.w);
|
|
13
|
+
header(c, headerModel(state));
|
|
14
|
+
const top = 2, bottom = c.h - 1;
|
|
15
|
+
const railW = bp === 'compact' ? 0 : Math.min(48, Math.floor(c.w * 0.32));
|
|
16
|
+
const chatW = c.w - railW;
|
|
17
|
+
const bc = mix(t.border, t.ai, 0.55);
|
|
18
|
+
// Live Claude session (task-1302): render it in the chat area, with the navigable coin list beside
|
|
19
|
+
// it (task-1313) so the user can see and change the hovered coin while chatting.
|
|
20
|
+
if (state.analyst) {
|
|
21
|
+
const focused = state.aiFocused === true;
|
|
22
|
+
// task-1313 part F: when the finished turn is a list-of-coins answer, resolve each mint against the
|
|
23
|
+
// app's LIVE data layer (never the model's text) and render it as a real table inside the chat. The
|
|
24
|
+
// table is "active" (↑↓ moves its cursor, ⏎ jumps, o opens a PAPER ticket) when the input is blurred.
|
|
25
|
+
const results = state.analyst.hasResults ? resolveAnalystResults(state, state.analyst.results) : [];
|
|
26
|
+
const resultsActive = results.length > 0 && !focused && state.aiResultsActive === true;
|
|
27
|
+
renderAnalystSession(c, 0, top, chatW, bottom - top, state.analyst, {
|
|
28
|
+
frame: state.vfx.frame, code: state.vfx.code, calm: isCalm(state.vfx),
|
|
29
|
+
input: state.aiInput ?? '', inputFocused: focused,
|
|
30
|
+
scroll: state.aiScroll, revealAll: state.aiRevealAll,
|
|
31
|
+
results, resultsCursor: state.analyst.resultsCursor, resultsActive,
|
|
32
|
+
inputHint: focused ? 'type your question… ⏎ send · esc blur · analysis not advice'
|
|
33
|
+
: results.length ? 'i ask · ↑↓ pick coin · ⏎ open · o ticket · esc back'
|
|
34
|
+
: 'i ask · ⏎ focus · ↑↓ pick coin · PgUp/Dn scroll',
|
|
35
|
+
});
|
|
36
|
+
if (railW)
|
|
37
|
+
renderCoinList(c, chatW, top, railW, bottom - top, state);
|
|
38
|
+
footer(c, analystFooter(state, focused), state.theme.name, isCalm(state.vfx));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// task-1311 (QA M13): the canned SIGMA/GOAT/BOME conversation is seeded demo data. Never show it
|
|
42
|
+
// next to live data — in live mode the empty tab says how to start a real conversation instead.
|
|
43
|
+
if (state.screener.source !== 'fixtures') {
|
|
44
|
+
const live = state.aiFocused === true;
|
|
45
|
+
const b = c.box(0, top, chatW, bottom - top, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'read-only · local claude CLI · on demand', captionFg: t.dim, border: bc });
|
|
46
|
+
const bbg = b.bg;
|
|
47
|
+
const bin = b.w - 2;
|
|
48
|
+
let by = b.y + 1;
|
|
49
|
+
const say2 = (s) => { for (const w2 of wrap(s, bin - 2)) {
|
|
50
|
+
if (by >= b.y + b.h - 3)
|
|
51
|
+
break;
|
|
52
|
+
c.text(b.x + 2, by, w2, { fg: t.muted, bg: bbg }, bin - 2);
|
|
53
|
+
by += 1;
|
|
54
|
+
} };
|
|
55
|
+
say2('No conversation yet.');
|
|
56
|
+
by += 1;
|
|
57
|
+
say2("Press i to ask claude about the market, a token, or your watchlist. It can read the live screener, a token's holders / tape / RugCheck, and your watchlist — read-only, never trades.");
|
|
58
|
+
const iy = b.y + b.h - 2;
|
|
59
|
+
c.hline(b.x, iy - 1, b.w, { fg: bc, bg: bbg, soft: true });
|
|
60
|
+
c.put(b.x, iy - 1, '├', { fg: bc, bg: bbg, soft: true });
|
|
61
|
+
c.put(b.x + b.w - 1, iy - 1, '┤', { fg: bc, bg: bbg, soft: true });
|
|
62
|
+
const typed = state.aiInput ?? '';
|
|
63
|
+
c.text(b.x + 1, iy, '▌', { fg: t.ai, bg: bbg, b: true, soft: true });
|
|
64
|
+
if (typed)
|
|
65
|
+
c.text(b.x + 2, iy, typed + (live ? '█' : ''), { fg: t.text, bg: bbg }, bin - 2);
|
|
66
|
+
else
|
|
67
|
+
c.text(b.x + 2, iy, live ? '█ type your question… ⏎ send · esc blur' : 'press i to ask claude · ⏎ to focus', { fg: t.dim, bg: bbg, i: true }, bin - 2);
|
|
68
|
+
if (railW)
|
|
69
|
+
renderCoinList(c, chatW, top, railW, bottom - top, state);
|
|
70
|
+
footer(c, analystFooter(state, live), state.theme.name, isCalm(state.vfx));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const cb = c.box(0, top, chatW, bottom - top, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'session 14m · 11 tool calls · $0.00 (your CLI)', border: bc });
|
|
74
|
+
const bg = cb.bg;
|
|
75
|
+
const inner = cb.w - 2;
|
|
76
|
+
const fx = state.fx;
|
|
77
|
+
const picks = fx.tokens.filter((k) => k.org >= 55 && k.top10 < 30 && !k.mintAuth).slice(0, 5);
|
|
78
|
+
const convo = [
|
|
79
|
+
{ role: 'user', text: 'screen for tokens under $5M mcap with rising holders, organic score over 60, and no bundler footprint. rank them.' },
|
|
80
|
+
{ role: 'tool', text: 'screener_query(mcap<5M, org>60, bundled<10) 38ms · get_holders_delta(5 tokens, 6h) 212ms · get_dev_history(5) 540ms' },
|
|
81
|
+
{ role: 'table', rows: picks },
|
|
82
|
+
{ role: 'ai', text: `Ranked by holder velocity × organic volume. ${picks[0]?.symbol} leads because new wallets outpace sells 3:1 and the dev has no prior rugs; ${picks[1]?.symbol} has the better chart but 2 wallets took 11% in block 1, which I weight down. None are "safe", all are sub-$5M memes.` },
|
|
83
|
+
{ role: 'user', text: `what would make you flip bullish on ${picks[2]?.symbol}?` },
|
|
84
|
+
{ role: 'ai', text: `Three things, in order: liquidity above $150K (currently ${fmtUsd(picks[2]?.liq || 0, { digits: 0 })}), top-10 below 25%, and a 6h candle closing above ${fmtPrice((picks[2]?.price || 0) * 1.18)} on organic volume. I added those as a watch rule; you'll get an alert, not a trade.` },
|
|
85
|
+
{ role: 'user', text: 'draft the thesis as a note for my watchlist' },
|
|
86
|
+
{ role: 'ai', text: `Saved to watchlist note for ${picks[2]?.symbol}: "Range-bound memecoin with improving holder spread; entry only on liq > 150K + top10 < 25%; invalidate on dev sell or liq < 60K." Edit with n.` },
|
|
87
|
+
];
|
|
88
|
+
const ai = state.ai ?? { phase: 'done', cut: convo.length, revealed: 0 };
|
|
89
|
+
const frame = state.vfx.frame;
|
|
90
|
+
const code = state.vfx.code;
|
|
91
|
+
const cut = ai.phase === 'done' ? convo.length : ai.cut;
|
|
92
|
+
let revealedLeft = ai.revealed;
|
|
93
|
+
let cy = cb.y;
|
|
94
|
+
const inputY = cb.y + cb.h - 2;
|
|
95
|
+
for (let li = 0; li < convo.length; li++) {
|
|
96
|
+
const ln = convo[li];
|
|
97
|
+
if (cy >= inputY - 1)
|
|
98
|
+
break;
|
|
99
|
+
if (li > cut && revealedLeft <= 0)
|
|
100
|
+
break;
|
|
101
|
+
const streaming = li >= cut;
|
|
102
|
+
if (streaming && ai.phase === 'thinking') {
|
|
103
|
+
c.text(cb.x + 2, cy, spinnerGlyph(frame), { fg: t.ai, bg, b: true });
|
|
104
|
+
const status = ['Thinking', 'Querying screener', 'Reading dev history', 'Decrypting'][Math.floor(frame / 24) % 4];
|
|
105
|
+
c.text(cb.x + 4, cy, `${status}…`, { fg: t.ai, bg, b: true });
|
|
106
|
+
c.text(cb.x + 5 + status.length, cy, `(esc to interrupt · ${(frame / 30).toFixed(1)}s · ↓ ${Math.floor(frame * 11)} tokens)`, { fg: t.dim, bg }, inner - status.length - 6);
|
|
107
|
+
cy += 1;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (ln.role === 'user') {
|
|
111
|
+
c.text(cb.x + 1, cy, '›', { fg: t.accent, bg, b: true });
|
|
112
|
+
for (const w2 of wrap(ln.text, inner - 3)) {
|
|
113
|
+
if (cy >= inputY - 1)
|
|
114
|
+
break;
|
|
115
|
+
c.text(cb.x + 3, cy, w2, { fg: t.text, bg, b: true });
|
|
116
|
+
cy += 1;
|
|
117
|
+
}
|
|
118
|
+
cy += 1;
|
|
119
|
+
}
|
|
120
|
+
else if (ln.role === 'tool') {
|
|
121
|
+
for (const w2 of wrap('↳ ' + ln.text, inner - 3)) {
|
|
122
|
+
if (cy >= inputY - 1)
|
|
123
|
+
break;
|
|
124
|
+
c.text(cb.x + 3, cy, w2, { fg: t.ai, bg, i: true });
|
|
125
|
+
cy += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (ln.role === 'table') {
|
|
129
|
+
cy += 1;
|
|
130
|
+
const cols = [['#', 3, 'r'], ['TOKEN', 11, 'l'], ['MCAP', 8, 'r'], ['HLD 6H', 8, 'r'], ['ORG', 4, 'r'], ['1H', 7, 'r'], ['24H', 8, 'r'], ['LIQ', 8, 'r'], ['TREND', 14, 'l']];
|
|
131
|
+
let hx = cb.x + 3;
|
|
132
|
+
for (const [lab, w, al] of cols) {
|
|
133
|
+
if (al === 'r')
|
|
134
|
+
c.textR(hx, cy, w, lab, { fg: t.dim, bg });
|
|
135
|
+
else
|
|
136
|
+
c.text(hx, cy, lab, { fg: t.dim, bg });
|
|
137
|
+
hx += w + 1;
|
|
138
|
+
}
|
|
139
|
+
cy += 1;
|
|
140
|
+
ln.rows.forEach((k, i) => {
|
|
141
|
+
if (cy >= inputY - 1)
|
|
142
|
+
return;
|
|
143
|
+
let rx = cb.x + 3;
|
|
144
|
+
const rowBg = i % 2 ? mix(bg, t.panelAlt, 1) : bg;
|
|
145
|
+
c.fill(cb.x + 2, cy, inner - 2, 1, ' ', { bg: rowBg });
|
|
146
|
+
c.textR(rx, cy, 3, String(i + 1), { fg: t.dim, bg: rowBg });
|
|
147
|
+
rx += 4;
|
|
148
|
+
c.text(rx, cy, k.symbol, { fg: t.text, bg: rowBg, b: true }, 11);
|
|
149
|
+
rx += 12;
|
|
150
|
+
c.textR(rx, cy, 8, fmtUsd(k.mcap, { digits: 1 }), { fg: t.text, bg: rowBg });
|
|
151
|
+
rx += 9;
|
|
152
|
+
c.textR(rx, cy, 8, fmtDeltaDir(k.holders24), { fg: k.holders24 >= 0 ? t.up : t.down, bg: rowBg });
|
|
153
|
+
rx += 9;
|
|
154
|
+
c.textR(rx, cy, 4, String(k.org), { fg: k.org >= 70 ? t.up : t.warn, bg: rowBg });
|
|
155
|
+
rx += 5;
|
|
156
|
+
heatPct(c, rx, cy, 7, k.chg.h1, rowBg);
|
|
157
|
+
rx += 8;
|
|
158
|
+
heatPct(c, rx, cy, 8, k.chg.h24, rowBg);
|
|
159
|
+
rx += 9;
|
|
160
|
+
c.textR(rx, cy, 8, fmtUsd(k.liq, { digits: 1 }), { fg: t.text, bg: rowBg });
|
|
161
|
+
rx += 9;
|
|
162
|
+
const sw = Math.min(14, cb.x + cb.w - 2 - rx);
|
|
163
|
+
if (sw >= 4)
|
|
164
|
+
sparkline(c, rx, cy, sw, k.spark, { bg: rowBg });
|
|
165
|
+
cy += 1;
|
|
166
|
+
});
|
|
167
|
+
cy += 1;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Claude speaks in coral; the streaming answer decrypts through code glyphs
|
|
171
|
+
c.text(cb.x + 1, cy, '✦', { fg: t.ai, bg, b: true });
|
|
172
|
+
for (const w2 of wrap(ln.text, inner - 3)) {
|
|
173
|
+
if (cy >= inputY - 1)
|
|
174
|
+
break;
|
|
175
|
+
if (streaming) {
|
|
176
|
+
const n = Math.min(revealedLeft, w2.length);
|
|
177
|
+
decryptText(c, cb.x + 3, cy, w2, n, frame, { fg: t.ai, bg }, inner - 3, code);
|
|
178
|
+
revealedLeft -= w2.length + 1;
|
|
179
|
+
cy += 1;
|
|
180
|
+
if (revealedLeft <= -6)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
c.text(cb.x + 3, cy, w2, { fg: t.ai, bg });
|
|
185
|
+
cy += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
cy += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (inputY - 2 > cy)
|
|
192
|
+
c.rainZone(cb.x + 1, cy, inner, inputY - 2 - cy, 2);
|
|
193
|
+
c.hline(cb.x, inputY - 1, cb.w, { fg: bc, bg, soft: true });
|
|
194
|
+
c.put(cb.x, inputY - 1, '├', { fg: bc, bg, soft: true });
|
|
195
|
+
c.put(cb.x + cb.w - 1, inputY - 1, '┤', { fg: bc, bg, soft: true });
|
|
196
|
+
// task-1311 (QA B3): the input line captures typed text even before a session exists, with a cursor
|
|
197
|
+
// when focused. Keystrokes reach here only while the box is focused, so nothing types by accident.
|
|
198
|
+
// When idle (not focused, nothing typed) the line keeps its original placeholder so the demo render
|
|
199
|
+
// is unchanged — the focus affordances only appear once the user engages.
|
|
200
|
+
const focused = state.aiFocused === true;
|
|
201
|
+
const typed = state.aiInput ?? '';
|
|
202
|
+
c.text(cb.x + 1, inputY, '▌', { fg: t.ai, bg, b: true, soft: true });
|
|
203
|
+
if (typed)
|
|
204
|
+
c.text(cb.x + 2, inputY, typed + (focused ? '█' : ''), { fg: t.text, bg }, inner - 2);
|
|
205
|
+
else if (focused)
|
|
206
|
+
c.text(cb.x + 2, inputY, '█ type your question… ⏎ send · esc blur', { fg: t.dim, bg, i: true }, inner - 2);
|
|
207
|
+
else
|
|
208
|
+
c.text(cb.x + 2, inputY, 'ask anything about the market, a token, or your watchlist… (/screen /explain /rank /thesis /backtest)', { fg: t.dim, bg, i: true }, inner - 2);
|
|
209
|
+
if (railW)
|
|
210
|
+
renderContextRail(c, chatW, top, railW, bottom - top, state);
|
|
211
|
+
footer(c, analystFooter(state, focused), state.theme.name, isCalm(state.vfx));
|
|
212
|
+
}
|
|
213
|
+
/** Footer keys for the analyst screen — focus state decides whether `q`/screens show or `esc blur`. */
|
|
214
|
+
function analystFooter(state, focused) {
|
|
215
|
+
if (state.analyst?.busy)
|
|
216
|
+
return [['⏎', 'queue'], ['esc', 'interrupt'], ['ctrl+c', 'quit']];
|
|
217
|
+
if (focused)
|
|
218
|
+
return [['⏎', 'send'], ['esc', state.aiInput ? 'clear' : 'blur'], ['ctrl+c', 'quit']];
|
|
219
|
+
if (state.analyst)
|
|
220
|
+
return [['i', 'ask'], ['esc', 'back'], ['tab', 'screens'], ['q', 'quit']];
|
|
221
|
+
// Idle demo tab (no session, not focused): the original footer, kept for prototype-v2 parity.
|
|
222
|
+
return [['⏎', 'send'], ['↑↓', 'history'], ['ctrl+l', 'clear'], ['esc', 'back'], ['tab', 'screens'], ['q', 'quit']];
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The right-hand navigable COIN LIST beside the chat (task-1313): the live screener rows with the
|
|
226
|
+
* hovered coin highlighted (↑↓ changes it, which updates the analyst's context), plus a short
|
|
227
|
+
* "analyst did X" log of applied UI actions. Degrades to nothing when the panel is too narrow (the
|
|
228
|
+
* caller passes w=0 in compact mode and the chat takes the full width).
|
|
229
|
+
*/
|
|
230
|
+
export function renderCoinList(c, x, y, w, h, state) {
|
|
231
|
+
const t = c.theme;
|
|
232
|
+
const live = state.screener.source !== 'fixtures';
|
|
233
|
+
const rows = live ? screenerView(state.screener) : [];
|
|
234
|
+
const sel = Math.max(0, Math.min(Math.max(0, rows.length - 1), state.selected || 0));
|
|
235
|
+
const box = c.box(x, y, w, h, { title: 'COINS', titleFg: t.text, caption: live ? '↑↓ pick · ⏎ open' : 'live only', captionFg: t.dim });
|
|
236
|
+
const bg = box.bg;
|
|
237
|
+
const inner = box.w - 2;
|
|
238
|
+
const log = state.analystActions ?? [];
|
|
239
|
+
// Reserve a short footer for the "analyst did X" log: a divider, a label, up to two entries.
|
|
240
|
+
const logRows = log.length ? 2 + Math.min(2, log.length) : 0;
|
|
241
|
+
const listBottom = box.y + box.h - logRows;
|
|
242
|
+
let ry = box.y;
|
|
243
|
+
if (!rows.length) {
|
|
244
|
+
c.text(box.x + 1, ry, live ? 'no rows yet…' : 'coin list is live-mode only', { fg: t.dim, bg, i: true }, inner);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const cap = Math.max(1, listBottom - box.y);
|
|
248
|
+
const startIdx = Math.max(0, Math.min(sel - Math.floor(cap / 2), Math.max(0, rows.length - cap)));
|
|
249
|
+
for (let i = startIdx; i < rows.length && ry < listBottom; i++) {
|
|
250
|
+
const r = rows[i];
|
|
251
|
+
const on = i === sel;
|
|
252
|
+
const rowBg = on ? mix(bg, t.accent, 0.3) : i % 2 ? mix(bg, t.panelAlt, 1) : bg;
|
|
253
|
+
c.fill(box.x + 1, ry, inner, 1, ' ', { bg: rowBg });
|
|
254
|
+
let rx = box.x + 1;
|
|
255
|
+
c.text(rx, ry, on ? '▸' : ' ', { fg: t.accent, bg: rowBg, b: true });
|
|
256
|
+
rx += 1;
|
|
257
|
+
c.text(rx, ry, r.symbol, { fg: t.text, bg: rowBg, b: on }, 9);
|
|
258
|
+
rx += 9;
|
|
259
|
+
const chg = r.chg.h24 ?? 0;
|
|
260
|
+
c.textR(rx, ry, 7, fmtPctDir(chg), { fg: chg >= 0 ? t.up : t.down, bg: rowBg });
|
|
261
|
+
rx += 8;
|
|
262
|
+
const liqW = Math.max(0, box.x + box.w - 1 - rx);
|
|
263
|
+
if (liqW >= 6)
|
|
264
|
+
c.textR(rx, ry, liqW, 'L' + fmtUsd(r.liq || 0, { digits: 0 }), { fg: t.dim, bg: rowBg });
|
|
265
|
+
ry += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (log.length) {
|
|
269
|
+
c.hline(box.x, listBottom, box.w, { fg: t.border, bg, soft: true });
|
|
270
|
+
c.put(box.x, listBottom, '├', { fg: t.border, bg, soft: true });
|
|
271
|
+
c.put(box.x + box.w - 1, listBottom, '┤', { fg: t.border, bg, soft: true });
|
|
272
|
+
c.text(box.x + 1, listBottom, ' ANALYST DID ', { fg: t.ai, bg });
|
|
273
|
+
let ly = listBottom + 1;
|
|
274
|
+
for (const l of log.slice(-2)) {
|
|
275
|
+
if (ly >= box.y + box.h)
|
|
276
|
+
break;
|
|
277
|
+
c.text(box.x + 1, ly, '✓ ' + l, { fg: t.ai, bg }, inner);
|
|
278
|
+
ly += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** The right-hand "what claude can see" rail: tool list, firewall note, guard rail, and a market pulse. */
|
|
283
|
+
export function renderContextRail(c, x, y, w, h, state) {
|
|
284
|
+
const t = c.theme;
|
|
285
|
+
const fx = state.fx;
|
|
286
|
+
const rb = c.box(x, y, w, h, { title: 'CONTEXT', caption: 'what claude can see' });
|
|
287
|
+
let ry = rb.y;
|
|
288
|
+
const items = [
|
|
289
|
+
['tools', 'screener_query · get_token · get_holders · get_trades · get_dev_history · rugcheck · watchlist_*'],
|
|
290
|
+
['firewall', 'token names, descriptions, socials wrapped as untrusted data; instructions inside them are ignored'],
|
|
291
|
+
['never', 'sign, send, or build a transaction. Trade panel requires your explicit confirm (v2).'],
|
|
292
|
+
['model', 'your local claude CLI · session kept warm · no keys shipped by dexterm'],
|
|
293
|
+
];
|
|
294
|
+
for (const [k, v] of items) {
|
|
295
|
+
c.text(rb.x + 1, ry, k.toUpperCase(), { fg: k === 'never' ? t.warn : t.dim, bg: rb.bg });
|
|
296
|
+
ry += 1;
|
|
297
|
+
for (const w2 of wrap(v, rb.w - 2)) {
|
|
298
|
+
if (ry >= rb.y + rb.h)
|
|
299
|
+
break;
|
|
300
|
+
c.text(rb.x + 1, ry, w2, { fg: k === 'never' ? t.warn : t.text, bg: rb.bg });
|
|
301
|
+
ry += 1;
|
|
302
|
+
}
|
|
303
|
+
ry += 1;
|
|
304
|
+
}
|
|
305
|
+
c.hline(rb.x, ry, rb.w, { fg: t.border, bg: rb.bg, soft: true });
|
|
306
|
+
ry += 1;
|
|
307
|
+
c.text(rb.x + 1, ry, 'MARKET PULSE', { fg: t.dim, bg: rb.bg });
|
|
308
|
+
c.heading(rb.x + 1, ry, 12);
|
|
309
|
+
ry += 1;
|
|
310
|
+
// task-1311 (QA M13): in live mode the pulse is the live screener's top rows — never the 2024
|
|
311
|
+
// fixture tickers (SIGMA/GOAT/…), which must not sit next to live data.
|
|
312
|
+
const live = state.screener.source !== 'fixtures';
|
|
313
|
+
const pulse = live
|
|
314
|
+
? state.screener.rows.slice(0, 8).map((r) => ({ symbol: r.symbol, spark: r.spark, chg24: r.chg.h24 ?? 0 }))
|
|
315
|
+
: fx.tokens.slice(0, 8).map((k) => ({ symbol: k.symbol, spark: k.spark, chg24: k.chg.h24 }));
|
|
316
|
+
for (const k of pulse) {
|
|
317
|
+
if (ry >= rb.y + rb.h)
|
|
318
|
+
break;
|
|
319
|
+
c.text(rb.x + 1, ry, k.symbol, { fg: t.text, bg: rb.bg, b: true }, 9);
|
|
320
|
+
sparkline(c, rb.x + 11, ry, rb.w - 22, k.spark, { bg: rb.bg });
|
|
321
|
+
c.textR(rb.x + rb.w - 9, ry, 8, fmtPctDir(k.chg24), { fg: k.chg24 >= 0 ? t.up : t.down, bg: rb.bg });
|
|
322
|
+
ry += 1;
|
|
323
|
+
}
|
|
324
|
+
if (ry + 1 < rb.y + rb.h)
|
|
325
|
+
c.rainZone(rb.x + 1, ry + 1, rb.w - 2, rb.y + rb.h - ry - 1, 2);
|
|
326
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// dexterm — boot sequence: full-screen rain, glitching DEXTERM logo, "jacking in" status lines.
|
|
2
|
+
// state.boot.t ∈ [0,1] drives it. The logo and the status box clear their own rain zone so the
|
|
3
|
+
// rain never crosses them (VISUAL-V2 §3 "Boot").
|
|
4
|
+
import { Canvas, bootLogo, decryptText, spinnerGlyph, mix } from '../../../tui/src/index.js';
|
|
5
|
+
// task-1311 (QA M13): the result column is the boot's intro texture, not a health report — it must
|
|
6
|
+
// not fabricate latencies / rpm before a single request has gone out. Honest status words only.
|
|
7
|
+
const LIVE_STEPS = [
|
|
8
|
+
['dexscreener', 'GET /latest/dex/search?q=solana', 'link'],
|
|
9
|
+
['jupiter', 'lite-api tokens/v2 + price/v3', 'link'],
|
|
10
|
+
['geckoterminal', 'ohlcv/hour · 60 candles', 'link'],
|
|
11
|
+
['rugcheck', 'summary · mint/freeze/LP', 'link'],
|
|
12
|
+
['pumpportal', 'ws newToken · migration', 'link'],
|
|
13
|
+
['claude -p', '--output-format stream-json · tools', 'local'],
|
|
14
|
+
['data store', 'sqlite cache · limiter · health registry', 'init'],
|
|
15
|
+
];
|
|
16
|
+
const FIXTURE_STEPS = [
|
|
17
|
+
...LIVE_STEPS.slice(0, 6),
|
|
18
|
+
['fixtures', '34 tokens · 20 pairs · 6 watch · seed 7', 'loaded'],
|
|
19
|
+
];
|
|
20
|
+
export function renderBoot(c, state) {
|
|
21
|
+
const t = c.theme;
|
|
22
|
+
const v = state.vfx;
|
|
23
|
+
const p = state.boot ? state.boot.t : 1;
|
|
24
|
+
const steps = state.screener.source === 'fixtures' ? FIXTURE_STEPS : LIVE_STEPS;
|
|
25
|
+
c.fill(0, 0, c.w, c.h, ' ', { bg: t.bg });
|
|
26
|
+
c.rainZone(0, 0, c.w, c.h, 1);
|
|
27
|
+
const cx = Math.floor(c.w / 2);
|
|
28
|
+
const logoY = Math.max(2, Math.floor(c.h * 0.22));
|
|
29
|
+
// logo settles as p grows; it glitches hard at first, then only on sporadic frames
|
|
30
|
+
const intensity = p < 0.35 ? 0.9 - p : p < 0.85 ? 0.25 : 0.12;
|
|
31
|
+
const logo = bootLogo(c, cx, logoY, v, intensity);
|
|
32
|
+
const tag = 'terminal dex screener · claude inside';
|
|
33
|
+
c.textC(0, logoY + 6, c.w, tag, { fg: t.muted, bg: t.bg });
|
|
34
|
+
c.heading(cx - Math.floor(tag.length / 2), logoY + 6, tag.length);
|
|
35
|
+
// keep the logo column clear of rain so it reads
|
|
36
|
+
c.noRain(logo.x - 2, logo.y - 1, logo.w + 4, 9);
|
|
37
|
+
// jacking-in lines reveal one by one between p=0.2 and p=0.85
|
|
38
|
+
const boxW = Math.min(c.w - 8, 76);
|
|
39
|
+
const bx = cx - Math.floor(boxW / 2);
|
|
40
|
+
let y = logoY + 9;
|
|
41
|
+
const frame = v.frame;
|
|
42
|
+
const per = 0.65 / steps.length;
|
|
43
|
+
const title = ' JACKING IN ';
|
|
44
|
+
c.text(bx, y, title, { fg: '#000000', bg: t.borderFocus, b: true });
|
|
45
|
+
c.text(bx + title.length + 1, y, `${c.w}×${c.h} · ${state.colorDepth} · ${state.theme.name}`, { fg: t.dim, bg: t.bg });
|
|
46
|
+
c.noRain(bx - 1, y, boxW + 2, 2 + steps.length + 3);
|
|
47
|
+
y += 2;
|
|
48
|
+
for (let i = 0; i < steps.length; i++) {
|
|
49
|
+
const [name, what, result] = steps[i];
|
|
50
|
+
const start = 0.2 + i * per;
|
|
51
|
+
if (p < start)
|
|
52
|
+
break;
|
|
53
|
+
const local = Math.min(1, (p - start) / per);
|
|
54
|
+
const line = `${name.padEnd(14)} ${what}`;
|
|
55
|
+
const revealed = Math.floor(local * 1.6 * line.length);
|
|
56
|
+
const claude = name === 'claude -p';
|
|
57
|
+
c.text(bx, y, '▸', { fg: claude ? t.ai : t.accent, bg: t.bg, b: true });
|
|
58
|
+
decryptText(c, bx + 2, y, line, revealed, frame, { fg: claude ? t.ai : t.text, bg: t.bg }, boxW - 20, v.code);
|
|
59
|
+
if (local >= 0.7) {
|
|
60
|
+
const ok = result.startsWith('ok') || result === 'connected' || result === 'loaded' || result.startsWith('warm');
|
|
61
|
+
c.textR(bx, y, boxW, result, { fg: ok ? t.up : t.warn, bg: t.bg, b: true });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
c.textR(bx, y, boxW, spinnerGlyph(frame) + ' ', { fg: t.ai, bg: t.bg, b: true });
|
|
65
|
+
}
|
|
66
|
+
y += 1;
|
|
67
|
+
}
|
|
68
|
+
if (p >= 0.9) {
|
|
69
|
+
y += 1;
|
|
70
|
+
const msg = '◉ JACKED IN · press any key';
|
|
71
|
+
c.text(bx, y, msg, { fg: t.borderFocus, bg: mix(t.bg, t.borderFocus, 0.12), b: true });
|
|
72
|
+
c.heading(bx, y, msg.length);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// dexterm — BUBBLE MAP screen (task-1340, wallet intel W3/W4). The terminal rendering of the holder graph the
|
|
2
|
+
// approved mocks showed (research/wallet-intel/mock/out): packed half-row discs (area ∝ share, colour = rule),
|
|
3
|
+
// a CLUSTERS table, sample-captioned HOLDER INSIGHTS and the WHY THIS LABEL panel that prints every rule with
|
|
4
|
+
// its confidence and evidence. Three honest states: loading, unavailable (reason + fix), ok (possibly degraded:
|
|
5
|
+
// keyless runs show holders + static labels and say what a free Helius key adds). Pure renderer.
|
|
6
|
+
import { Canvas, badge, statGrid, header, footer, breakpoint, isCalm, fmtNum, shortAddr, wrap, layoutBubbles, drawBubbles, drawTreemap, legendRow, kindColor, sampleCaption, } from '../../../tui/src/index.js';
|
|
7
|
+
import { headerModel } from "../state.js";
|
|
8
|
+
import { unavailableText } from "../live/intel.js";
|
|
9
|
+
export function renderBubbles(c, state) {
|
|
10
|
+
const t = c.theme;
|
|
11
|
+
const bp = breakpoint(c.w);
|
|
12
|
+
header(c, headerModel(state));
|
|
13
|
+
const ui = state.intelUi ?? null;
|
|
14
|
+
const entry = ui && state.live?.intel ? state.live.intel.entry(ui.mint) : null;
|
|
15
|
+
const top = 2, bottom = c.h - 1;
|
|
16
|
+
// ---- title strip
|
|
17
|
+
let y = top;
|
|
18
|
+
c.fill(0, y, c.w, 1, ' ', { bg: t.bg });
|
|
19
|
+
let x = c.text(1, y, '‹ ', { fg: t.muted, bg: t.bg });
|
|
20
|
+
const symbol = ui?.symbol ?? '?';
|
|
21
|
+
x = c.text(x, y, `$${symbol}`, { fg: t.borderFocus, bg: t.bg, b: true });
|
|
22
|
+
c.heading(3, y, symbol.length + 1);
|
|
23
|
+
if (ui?.name && bp !== 'compact')
|
|
24
|
+
x = c.text(x + 1, y, ui.name.slice(0, 28), { fg: t.muted, bg: t.bg });
|
|
25
|
+
x = badge(c, x + 2, y, 'BUBBLE MAP', t.accent, { on: t.bg, bold: true });
|
|
26
|
+
if (ui?.mint)
|
|
27
|
+
x = c.text(x + 2, y, shortAddr(ui.mint, 6), { fg: t.dim, bg: t.bg });
|
|
28
|
+
const vm = entry?.state === 'ok' ? entry.bubbles : null;
|
|
29
|
+
const r = entry?.result?.state === 'ok' ? entry.result : null;
|
|
30
|
+
const right = vm && r ? `${r.depth} scan · ${r.via.history ? `${r.via.holders} + helius history` : `${r.via.holders} only`}${r.fromCache ? ' · cached' : ''} ` : entry?.state === 'loading' ? 'scanning… ' : '';
|
|
31
|
+
if (right && c.w - right.length - 1 > x + 2) {
|
|
32
|
+
c.text(c.w - 1 - right.length, y, right, { fg: t.dim, bg: t.bg });
|
|
33
|
+
c.rainZone(x + 1, y, c.w - right.length - x - 3, 1, 1);
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
c.rainZone(x + 1, y, Math.max(0, c.w - x - 2), 1, 1);
|
|
37
|
+
y += 1;
|
|
38
|
+
const sideW = bp === 'compact' ? 0 : bp === 'wide' ? 46 : 38;
|
|
39
|
+
const mapW = c.w - sideW;
|
|
40
|
+
const mapH = bottom - y;
|
|
41
|
+
const view = ui?.view ?? 'bubbles';
|
|
42
|
+
const title = `${symbol} · ${view === 'treemap' ? 'TREEMAP' : 'BUBBLES'}${ui?.focus ? ` · zoom ${ui.focus}` : ''}`;
|
|
43
|
+
const mb = c.box(0, y, mapW, mapH, { title, caption: view === 'treemap' ? 'area ∝ share · colour = rule' : 'area ∝ share · colour = rule · ring = linked cluster', focus: true });
|
|
44
|
+
const legendH = bp === 'compact' ? 2 : 1;
|
|
45
|
+
const innerH = mb.h - legendH - 1;
|
|
46
|
+
if (!vm) {
|
|
47
|
+
// loading / unavailable: say so, never draw a fake map
|
|
48
|
+
const lines = [];
|
|
49
|
+
if (!ui) {
|
|
50
|
+
lines.push(['no token: open a token and press M for its bubble map', t.muted]);
|
|
51
|
+
}
|
|
52
|
+
else if (!entry || entry.state === 'loading') {
|
|
53
|
+
lines.push(['scanning holders…', t.accent]);
|
|
54
|
+
lines.push(['top accounts → owners → owner kinds, then funders + the launch window (Helius)', t.dim]);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const u = unavailableText(entry);
|
|
58
|
+
lines.push([`bubble map unavailable: ${u.reason}`, t.warn]);
|
|
59
|
+
if (u.fix)
|
|
60
|
+
lines.push([`fix: ${u.fix}`, t.muted]);
|
|
61
|
+
lines.push(['r rescans · esc back', t.dim]);
|
|
62
|
+
}
|
|
63
|
+
let ly = mb.y + Math.max(1, Math.floor(innerH / 2) - lines.length);
|
|
64
|
+
for (const [s, fg] of lines)
|
|
65
|
+
for (const ln of wrap(s, mb.w - 4)) {
|
|
66
|
+
if (ly >= mb.y + innerH)
|
|
67
|
+
break;
|
|
68
|
+
c.textC(mb.x, ly, mb.w, ln, { fg, bg: mb.bg, i: true });
|
|
69
|
+
ly += 1;
|
|
70
|
+
}
|
|
71
|
+
c.rainZone(mb.x + 1, mb.y, mb.w - 2, Math.max(0, ly - mb.y - lines.length - 1), 2);
|
|
72
|
+
}
|
|
73
|
+
else if (view === 'treemap') {
|
|
74
|
+
drawTreemap(c, mb.x + 1, mb.y, mb.w - 2, innerH, vm, { bg: mb.bg, selected: ui?.selected ?? null });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const key = `${entry?.at}|${mb.w}|${innerH}|${ui?.focus ?? ''}|${vm.holders.length}`;
|
|
78
|
+
let lay = ui?.layout && ui.layout.key === key ? ui.layout.lay : null;
|
|
79
|
+
if (!lay) {
|
|
80
|
+
lay = layoutBubbles(vm, mb.w - 2, innerH, { focus: ui?.focus ?? null });
|
|
81
|
+
if (ui)
|
|
82
|
+
ui.layout = { key, lay };
|
|
83
|
+
}
|
|
84
|
+
drawBubbles(c, mb.x + 1, mb.y, mb.w - 2, innerH, vm, lay, { bg: mb.bg, selected: ui?.selected ?? null });
|
|
85
|
+
if (lay.overflow)
|
|
86
|
+
c.text(mb.x + 1, mb.y, 'terminal too small to pack the map: grid view', { fg: t.warn, bg: mb.bg, i: true }, mb.w - 2);
|
|
87
|
+
}
|
|
88
|
+
const ly = mb.y + mb.h - legendH;
|
|
89
|
+
c.hline(mb.x, ly - 1, mb.w, { fg: t.border, bg: mb.bg, soft: true });
|
|
90
|
+
legendRow(c, mb.x + 1, ly, mb.w - 2, mb.bg);
|
|
91
|
+
if (bp === 'compact' && vm && ui?.selected != null && vm.holders[ui.selected]) {
|
|
92
|
+
const hd = vm.holders[ui.selected];
|
|
93
|
+
const first = hd.labels[0];
|
|
94
|
+
const cl0 = hd.cluster ? vm.clusters.find((k) => k.name === hd.cluster) : null;
|
|
95
|
+
const ev0 = cl0 ? cl0.evidence.find((e) => e.linked !== false && (e.a === hd.owner || e.b === hd.owner)) : null;
|
|
96
|
+
let sx = c.text(mb.x + 1, ly + 1, `${shortAddr(hd.owner, 4)} ${hd.pct.toFixed(2)}%`, { fg: t.borderFocus, bg: mb.bg, b: true });
|
|
97
|
+
sx = badge(c, sx + 1, ly + 1, hd.kind.toUpperCase(), kindColor(t, hd.kind), { on: mb.bg });
|
|
98
|
+
c.text(sx + 1, ly + 1, first ? `${first.label}: ${first.rule}` : ev0 ? `linked: ${ev0.rule}` : 'no rule matched · unlinked wallet', { fg: t.muted, bg: mb.bg }, mb.x + mb.w - 2 - sx - 1);
|
|
99
|
+
}
|
|
100
|
+
if (sideW)
|
|
101
|
+
sidePanels(c, mapW, y, sideW, mapH, vm, entry, ui);
|
|
102
|
+
const hints = [['esc', 'back'], ['j/k', 'select'], ['m', view === 'treemap' ? 'bubbles' : 'treemap'], ['z', ui?.focus ? 'unzoom' : 'zoom'], ['M', state.popups?.bubblesOpen?.() ? 'close popup' : 'popup'], ['D', 'deep scan'], ['r', 'rescan'], ['q', 'quit']];
|
|
103
|
+
footer(c, hints, t.name, isCalm(state.vfx));
|
|
104
|
+
}
|
|
105
|
+
function sidePanels(c, x, y, w, h, vm, entry, ui) {
|
|
106
|
+
const t = c.theme;
|
|
107
|
+
const r = entry?.result?.state === 'ok' ? entry.result : null;
|
|
108
|
+
const clH = Math.max(7, Math.min(12, Math.floor(h * 0.3)));
|
|
109
|
+
const insH = r?.degraded ? 13 : 11; // a degraded (keyless) scan prints its honest note inside the insights panel
|
|
110
|
+
const whyH = h - clH - insH;
|
|
111
|
+
// CLUSTERS
|
|
112
|
+
const cb = c.box(x, y, w, clH, { title: 'CLUSTERS', caption: vm ? `${vm.clusters.length} · ${vm.holders.length} wallets` : '' });
|
|
113
|
+
let cy = cb.y;
|
|
114
|
+
c.text(cb.x + 1, cy, 'ID KIND W % TOP WALLET', { fg: t.dim, bg: cb.bg }, cb.w - 2);
|
|
115
|
+
cy += 1;
|
|
116
|
+
const selOwner = vm && ui?.selected != null ? vm.holders[ui.selected]?.owner : null;
|
|
117
|
+
const sorted = vm ? vm.clusters.slice().sort((a, b) => b.pct - a.pct) : [];
|
|
118
|
+
for (const cl of sorted) {
|
|
119
|
+
if (cy >= cb.y + cb.h)
|
|
120
|
+
break;
|
|
121
|
+
const sel = !!selOwner && cl.members.includes(selOwner);
|
|
122
|
+
const rowBg = sel ? t.selBg : cb.bg;
|
|
123
|
+
c.fill(cb.x, cy, cb.w, 1, ' ', { bg: rowBg });
|
|
124
|
+
const color = kindColor(t, cl.kind);
|
|
125
|
+
let cx = c.text(cb.x + 1, cy, cl.name.padEnd(4), { fg: sel ? t.borderFocus : t.text, bg: rowBg, b: sel });
|
|
126
|
+
cx = c.text(cx + 1, cy, `${cl.members.length > 1 ? '●' : '○'} ${cl.kind}`.padEnd(11), { fg: color, bg: rowBg });
|
|
127
|
+
cx = c.textR(cx, cy, 3, String(cl.members.length), { fg: t.muted, bg: rowBg });
|
|
128
|
+
cx = c.textR(cx, cy, 6, `${cl.pct.toFixed(1)}`, { fg: cl.pct >= 10 ? t.warn : t.text, bg: rowBg });
|
|
129
|
+
c.text(cx + 2, cy, shortAddr(cl.members[0], 4), { fg: t.dim, bg: rowBg }, cb.w - (cx + 2 - cb.x) - 1);
|
|
130
|
+
cy += 1;
|
|
131
|
+
}
|
|
132
|
+
if (cy < cb.y + cb.h)
|
|
133
|
+
c.rainZone(cb.x + 1, cy, cb.w - 2, cb.y + cb.h - cy, 2);
|
|
134
|
+
// HOLDER INSIGHTS (sample-captioned; nulls when the history leg did not run)
|
|
135
|
+
const ib = c.box(x, y + clH, w, insH, { title: 'HOLDER INSIGHTS', caption: 'sample-bounded' });
|
|
136
|
+
let iy = ib.y;
|
|
137
|
+
if (r && vm) {
|
|
138
|
+
const I = r.analysis.insights;
|
|
139
|
+
const hist = I.coverage.launchTxs > 0;
|
|
140
|
+
const fund = I.coverage.fundingLookups > 0;
|
|
141
|
+
const pctS = (v) => (v == null ? 'n/a' : `${Number(v).toFixed(1)}%`);
|
|
142
|
+
iy += statGrid(c, ib.x + 1, iy, ib.w - 2, [
|
|
143
|
+
{ label: 'top10 raw', value: pctS(I.top10.byTokenAccount), sub: 'by token acct' },
|
|
144
|
+
// task-1367: ex-pool (no POOL/CURVE/VAULT/LOCK/BURN owners) is the headline; the cluster-adjusted figure rides in the sub
|
|
145
|
+
typeof I.top10.exPool === 'number'
|
|
146
|
+
? { label: 'top10 ex-pool', value: pctS(I.top10.exPool), sub: `adj ${pctS(I.top10.clusterAdjusted)} · -${I.top10.exPoolDropped ?? 0} venue`, style: { fg: I.top10.exPool >= 35 ? t.down : t.accent, b: true } }
|
|
147
|
+
: { label: 'top10 adj', value: pctS(I.top10.clusterAdjusted), sub: `excl ${I.top10.excludedCount} lbl`, style: { fg: t.accent, b: true } },
|
|
148
|
+
{ label: 'snipers', value: hist ? String(I.snipers.count) : 'n/a', sub: hist && I.snipers.count ? `${pctS(I.snipers.launchWindowSharePct)} at launch` : hist ? 'first 3 slots' : 'needs history' },
|
|
149
|
+
{ label: 'bundled', value: hist ? pctS(I.bundle.bundlePctOfTopSample) : 'n/a', sub: hist ? `${I.bundle.sameSlotCohorts} cohorts` : 'needs history' },
|
|
150
|
+
{ label: 'dev', value: I.dev.creatorPctNow != null ? pctS(I.dev.creatorPctNow) : I.dev.creatorUpperBoundPct != null ? `≤${pctS(I.dev.creatorUpperBoundPct)}` : 'n/a', sub: !hist ? 'needs history' : I.dev.creatorPctNow == null ? 'not in sample' : 'now', style: { fg: t.warn, b: true } },
|
|
151
|
+
{ label: 'fresh', value: fund ? pctS(I.fresh.freshAtBuyPctOfTopSample) : 'n/a', sub: fund ? `of ${pctS(I.fresh.measuredPct)} meas.` : 'needs history' },
|
|
152
|
+
], { bg: ib.bg, cols: 2 });
|
|
153
|
+
const big = I.bigHolder;
|
|
154
|
+
if (big)
|
|
155
|
+
for (const ln of wrap(`⚠ main risk: ${big.tag} ${shortAddr(big.owner, 4)} holds ${big.pct.toFixed(1)}%`, ib.w - 2)) {
|
|
156
|
+
if (iy >= ib.y + ib.h)
|
|
157
|
+
break;
|
|
158
|
+
c.text(ib.x + 1, iy, ln, { fg: t.down, bg: ib.bg, b: true });
|
|
159
|
+
iy += 1;
|
|
160
|
+
}
|
|
161
|
+
if (r.degraded)
|
|
162
|
+
for (const ln of wrap(`partial: ${r.degraded}`, ib.w - 2)) {
|
|
163
|
+
if (iy >= ib.y + ib.h)
|
|
164
|
+
break;
|
|
165
|
+
c.text(ib.x + 1, iy, ln, { fg: t.warn, bg: ib.bg, i: true });
|
|
166
|
+
iy += 1;
|
|
167
|
+
}
|
|
168
|
+
for (const ln of wrap(sampleCaption(vm, fmtNum), ib.w - 2)) {
|
|
169
|
+
if (iy >= ib.y + ib.h)
|
|
170
|
+
break;
|
|
171
|
+
c.text(ib.x + 1, iy, ln, { fg: t.dim, bg: ib.bg });
|
|
172
|
+
iy += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
c.text(ib.x + 1, iy, entry?.state === 'loading' ? 'scanning…' : 'no insights without a holder list', { fg: t.dim, bg: ib.bg, i: true }, ib.w - 2);
|
|
177
|
+
iy += 1;
|
|
178
|
+
}
|
|
179
|
+
if (iy < ib.y + ib.h)
|
|
180
|
+
c.rainZone(ib.x + 1, iy, ib.w - 2, ib.y + ib.h - iy, 2);
|
|
181
|
+
// WHY THIS LABEL
|
|
182
|
+
const wb = c.box(x, y + clH + insH, w, whyH, { title: '? WHY THIS LABEL', caption: 'rules, not facts' });
|
|
183
|
+
let wy = wb.y;
|
|
184
|
+
const hd = vm && ui?.selected != null ? vm.holders[ui.selected] ?? null : null;
|
|
185
|
+
if (!hd) {
|
|
186
|
+
c.text(wb.x + 1, wy, 'select a bubble (j/k) to see the rules behind its colour', { fg: t.dim, bg: wb.bg }, wb.w - 2);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let hx = c.text(wb.x + 1, wy, shortAddr(hd.owner, 6), { fg: t.borderFocus, bg: wb.bg, b: true });
|
|
190
|
+
hx = c.text(hx + 1, wy, `${hd.pct.toFixed(2)}%`, { fg: t.text, bg: wb.bg, b: true });
|
|
191
|
+
hx = badge(c, hx + 1, wy, hd.kind.toUpperCase(), kindColor(t, hd.kind), { on: wb.bg });
|
|
192
|
+
if (hd.cluster)
|
|
193
|
+
c.textR(wb.x + 1, wy, wb.w - 2, hd.cluster, { fg: t.muted, bg: wb.bg });
|
|
194
|
+
wy += 1;
|
|
195
|
+
// task-1367: the holder's class (POOL / CURVE / … / UNLABELLED) and the on-chain reason, first
|
|
196
|
+
const cls = r?.analysis.holders.find((h) => h.owner === hd.owner)?.cls ?? null;
|
|
197
|
+
if (cls) {
|
|
198
|
+
const cx2 = badge(c, wb.x + 1, wy, cls.tag, cls.tag === 'UNLABELLED' ? t.muted : cls.tag === 'DEV' || cls.tag === 'LINKED' ? t.warn : t.accent, { on: wb.bg, bold: true });
|
|
199
|
+
const lines = wrap(cls.reason + (cls.lock ? ` · ${cls.lock.unlock ?? cls.lock.note}` : ''), Math.max(8, wb.w - (cx2 - wb.x) - 2));
|
|
200
|
+
for (const [i, ln] of lines.entries()) {
|
|
201
|
+
if (wy >= wb.y + wb.h)
|
|
202
|
+
break;
|
|
203
|
+
c.text(i === 0 ? cx2 + 1 : wb.x + 3, wy, ln, { fg: t.muted, bg: wb.bg }, wb.w - 3);
|
|
204
|
+
wy += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (hd.funder) {
|
|
208
|
+
c.text(wb.x + 1, wy, `funded by ${shortAddr(hd.funder.address, 4)}${hd.funder.label ? ` (${hd.funder.label})` : ''}${hd.funder.blockTime ? ` · ${new Date(hd.funder.blockTime * 1000).toISOString().slice(0, 10)}` : ''}`, { fg: t.muted, bg: wb.bg }, wb.w - 2);
|
|
209
|
+
wy += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (hd.walletFirstSeen) {
|
|
212
|
+
c.text(wb.x + 1, wy, `first seen ${new Date(hd.walletFirstSeen * 1000).toISOString().slice(0, 10)} · funder not found in first txs`, { fg: t.muted, bg: wb.bg }, wb.w - 2);
|
|
213
|
+
wy += 1;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
c.text(wb.x + 1, wy, 'funding not looked up (outside the funded sample)', { fg: t.dim, bg: wb.bg }, wb.w - 2);
|
|
217
|
+
wy += 1;
|
|
218
|
+
}
|
|
219
|
+
wy += 1;
|
|
220
|
+
const cl = hd.cluster && vm ? vm.clusters.find((k) => k.name === hd.cluster) : null;
|
|
221
|
+
// a linked holder with no label of its own is explained by its cluster's rule (the evidence lines below say which)
|
|
222
|
+
const rules = hd.labels.length ? hd.labels : cl && cl.members.length > 1
|
|
223
|
+
? [{ label: `linked · ${cl.kind}`, rule: `no label of its own; linked into ${cl.name} (${cl.members.length} wallets, ${cl.pct.toFixed(1)}%) by the evidence below`, confidence: 'on-chain' }]
|
|
224
|
+
: [{ label: 'unlinked', rule: 'no rule matched: no shared funder, same-slot buy, transfer or creator link found in the sample', confidence: 'n/a' }];
|
|
225
|
+
const ev = cl ? cl.evidence.filter((e) => e.linked !== false && (e.a === hd.owner || e.b === hd.owner)) : [];
|
|
226
|
+
for (const rl of rules) {
|
|
227
|
+
if (wy >= wb.y + wb.h)
|
|
228
|
+
break;
|
|
229
|
+
c.text(wb.x + 1, wy, '●', { fg: kindColor(t, rl.label === 'LP' || rl.label === 'contract' ? 'pool' : rl.label === 'multi-token-top20' ? 'whale' : rl.label === 'fresh-at-buy' || rl.label === 'same-slot-created' ? 'plain' : rl.label), bg: wb.bg });
|
|
230
|
+
c.text(wb.x + 3, wy, rl.label, { fg: t.text, bg: wb.bg, b: true });
|
|
231
|
+
c.textR(wb.x + 1, wy, wb.w - 2, rl.confidence || '', { fg: rl.confidence === 'seed' ? t.warn : t.dim, bg: wb.bg });
|
|
232
|
+
wy += 1;
|
|
233
|
+
for (const ln of wrap(rl.rule, wb.w - 5)) {
|
|
234
|
+
if (wy >= wb.y + wb.h)
|
|
235
|
+
break;
|
|
236
|
+
c.text(wb.x + 3, wy, ln, { fg: t.muted, bg: wb.bg });
|
|
237
|
+
wy += 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const e of ev.slice(0, 3)) {
|
|
241
|
+
if (wy >= wb.y + wb.h)
|
|
242
|
+
break;
|
|
243
|
+
for (const ln of wrap(`↳ ${shortAddr(e.a, 4)} ↔ ${shortAddr(e.b, 4)}: ${e.rule}`, wb.w - 3)) {
|
|
244
|
+
if (wy >= wb.y + wb.h)
|
|
245
|
+
break;
|
|
246
|
+
c.text(wb.x + 2, wy, ln, { fg: t.dim, bg: wb.bg, i: true });
|
|
247
|
+
wy += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (wy + 1 < wb.y + wb.h) {
|
|
251
|
+
c.text(wb.x + 1, wb.y + wb.h - 1, 'labels are heuristics (rule + evidence) · never asserted as fact', { fg: t.dim, bg: wb.bg }, wb.w - 2);
|
|
252
|
+
if (wy < wb.y + wb.h - 2)
|
|
253
|
+
c.rainZone(wb.x + 1, wy + 1, wb.w - 2, wb.y + wb.h - 2 - wy, 2);
|
|
254
|
+
}
|
|
255
|
+
}
|