@gamaze/hicortex 0.23.0 → 0.23.2

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 (49) hide show
  1. package/assets/dashboard.html +455 -153
  2. package/dist/dashboard.d.ts +18 -7
  3. package/dist/dashboard.js +42 -10
  4. package/dist/dedup.js +2 -2
  5. package/dist/index.js +17 -2
  6. package/dist/learnings-identity.js +20 -1
  7. package/dist/llm.d.ts +12 -1
  8. package/dist/llm.js +14 -3
  9. package/dist/mcp-server.js +6 -2
  10. package/dist/mcp-stdio.d.ts +61 -3
  11. package/dist/mcp-stdio.js +272 -51
  12. package/dist/nightly.js +1 -1
  13. package/hermes-plugin/hicortex/provider.py +23 -0
  14. package/opencode-plugin/hicortex/index.ts +24 -1
  15. package/package.json +2 -1
  16. package/pi-extension/hicortex/index.ts +24 -1
  17. package/server.json +2 -2
  18. package/dist/eval/decay-eval.d.ts +0 -111
  19. package/dist/eval/decay-eval.js +0 -214
  20. package/dist/eval/dups.d.ts +0 -100
  21. package/dist/eval/dups.js +0 -174
  22. package/dist/eval/eval-clock.d.ts +0 -32
  23. package/dist/eval/eval-clock.js +0 -47
  24. package/dist/eval/eval-db.d.ts +0 -25
  25. package/dist/eval/eval-db.js +0 -67
  26. package/dist/eval/graph-eval.d.ts +0 -89
  27. package/dist/eval/graph-eval.js +0 -246
  28. package/dist/eval/importance-eval.d.ts +0 -85
  29. package/dist/eval/importance-eval.js +0 -286
  30. package/dist/eval/planted-eval.d.ts +0 -30
  31. package/dist/eval/planted-eval.js +0 -122
  32. package/dist/eval/planted-fixtures.d.ts +0 -107
  33. package/dist/eval/planted-fixtures.js +0 -283
  34. package/dist/eval/planted-harness.d.ts +0 -183
  35. package/dist/eval/planted-harness.js +0 -651
  36. package/dist/eval/ranking-battery.d.ts +0 -125
  37. package/dist/eval/ranking-battery.js +0 -289
  38. package/dist/eval/ranking-eval.d.ts +0 -61
  39. package/dist/eval/ranking-eval.js +0 -554
  40. package/dist/eval/ranking-fixtures.d.ts +0 -117
  41. package/dist/eval/ranking-fixtures.js +0 -485
  42. package/dist/eval/recall-sweep.d.ts +0 -87
  43. package/dist/eval/recall-sweep.js +0 -1030
  44. package/dist/eval/reflection-census.d.ts +0 -19
  45. package/dist/eval/reflection-census.js +0 -25
  46. package/dist/eval/relevance-eval.d.ts +0 -178
  47. package/dist/eval/relevance-eval.js +0 -2240
  48. package/dist/eval/run-eval.d.ts +0 -20
  49. package/dist/eval/run-eval.js +0 -299
@@ -76,7 +76,7 @@
76
76
  badged "best match" (server composite order kept — ranking is #425).
77
77
  All-below-floor renders "no matches for '<q>'" — NN junk never shows.
78
78
  - ONE-SCREEN LAYOUT: header + band + field fit the first viewport at
79
- ~1440×900 (MBP5 reference; 1280×800 verified). Band compacted; the
79
+ ~1440×900 (reference resolution; 1280×800 verified). Band compacted; the
80
80
  field takes the viewport remainder (clamp with a measured offset —
81
81
  see the #brainwrap CSS note). Scroll stays available as overflow.
82
82
 
@@ -345,9 +345,8 @@
345
345
  }
346
346
  .acct-btn:hover{ border-color:var(--green-mid); color:var(--green-mid); }
347
347
  .acct-btn:disabled{ opacity:0.5; cursor:default; }
348
- .acct-menu details{ border-top:1px solid var(--line); margin-top:10px; padding-top:8px; }
349
- .acct-menu summary{ cursor:pointer; color:var(--dim); font-size:13px; }
350
- .acct-menu summary:hover{ color:var(--green-mid); }
348
+ /* the acct-* snippet classes are shared by the account menu's token row
349
+ and the connect overlay's snippet blocks (#506 — one idiom, two hosts) */
351
350
  .acct-snippet{ margin-bottom:8px; }
352
351
  .acct-snippet pre{
353
352
  background:var(--panel); border:1px solid var(--line2); border-radius:7px;
@@ -363,12 +362,40 @@
363
362
  }
364
363
  .acct-link:hover{ background:var(--panel); }
365
364
  .acct-link.acct-danger:hover{ color:var(--red); }
366
- /* the upgrade CTA as a MENU ITEM (#409 fix round 2, owner ruling
367
- 2026-09-14: the corner is the account chip; upgrade lives INSIDE the
368
- menu — no standalone header button). Accent row in the #365 link style. */
369
- .acct-link.acct-upgrade{ color:var(--green-mid); font-weight:640; }
370
- .acct-link.acct-upgrade:hover{ background:color-mix(in srgb, var(--green) 8%, transparent); }
371
- .acct-link.acct-upgrade:disabled{ opacity:.5; cursor:default; }
365
+ /* the Subscription entry's upgrade pill (#371 round 3, owner: the menu's
366
+ two billing entries merge into ONE "Subscription" item — "it can have
367
+ an upgrade pill/label after it before any subscription is active").
368
+ The ENTRY always renders; the PILL is what the plan-unset eligibility
369
+ gates. Accent badge in the trigger's chip idiom. */
370
+ .acct-link .acct-pill{
371
+ margin-left:8px; font-size:9px; font-weight:640; letter-spacing:.08em; text-transform:uppercase;
372
+ color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 35%, transparent);
373
+ background:color-mix(in srgb, var(--green) 8%, transparent);
374
+ border-radius:999px; padding:1px 7px; vertical-align:1px;
375
+ }
376
+
377
+ /* ============ billing-return banner (#371) — the one-line acknowledgment
378
+ for ?billing=success|cancelled (the URLs the router stamps on every
379
+ checkout). A page-level element under the header, deliberately NOT a
380
+ toast: the returning buyer must still find it when they look back at
381
+ the page. Shown once — the param is stripped on show, so neither
382
+ dismissal nor a plain reload can re-show it. Theme tokens only. */
383
+ #billing-banner{
384
+ display:flex; align-items:center; justify-content:center; gap:10px;
385
+ margin:8px 16px 0; padding:7px 12px; border-radius:10px;
386
+ border:1px solid var(--line2); background:var(--panel);
387
+ font-size:12px; color:var(--dim); flex:none;
388
+ }
389
+ /* author display beats the UA's [hidden]{display:none} — without the
390
+ override the "hidden" banner rendered an EMPTY strip under the header
391
+ on every plain load (found live in preview). The #model-modal idiom. */
392
+ #billing-banner[hidden]{ display:none; }
393
+ #billing-banner[data-kind="success"]{
394
+ color:var(--green-mid);
395
+ border-color:color-mix(in srgb, var(--green) 30%, var(--line2));
396
+ }
397
+ #billing-banner button{ color:var(--faint); font-size:11px; padding:2px 4px; }
398
+ #billing-banner button:hover{ color:var(--text); }
372
399
 
373
400
  /* ============ the stats band — a WRAPPING CARD GRID ============
374
401
  Cards come in TWO widths only — the row is a positional grid
@@ -503,8 +530,9 @@
503
530
 
504
531
  .cap-of{ font-size:10.5px; color:var(--faint); font-variant-numeric:tabular-nums; }
505
532
  .bseg.warn .csub{ color:var(--amber); }
506
- /* Agents card: a faded amber Connect pill, bottom-right — opens the rail's
507
- connect drawer (the phase-1 stand-in for the seats CTA: no cap exists) */
533
+ /* Agents card: a faded amber Connect pill, bottom-right — opens the
534
+ full-cover connect overlay (#506; the phase-1 rail drawer stand-in for
535
+ the seats CTA is retired: no cap exists) */
508
536
  #lim-ag .limit-cta{
509
537
  display:inline-block; align-self:flex-end;
510
538
  color:var(--amber); border-color:color-mix(in srgb, var(--amber) 45%, transparent);
@@ -546,7 +574,7 @@
546
574
 
547
575
  /* ---- the brain-field ---- */
548
576
  /* ONE-SCREEN AT-REST LAYOUT (#409 polish, owner ruling 2026-09-13): header
549
- + stats band + field fit the first viewport at ~1440×900 (MBP5 is the
577
+ + stats band + field fit the first viewport at ~1440×900 (that is the
550
578
  reference; 1280×800 verified too). The field takes the viewport REMAINDER
551
579
  — the subtracted constant is the MEASURED header+band+paddings total on
552
580
  the reference store (Chrome 152, headless): 412px at the 4-column width,
@@ -766,24 +794,6 @@
766
794
  .ctx-detail .ctx-head{ display:flex; align-items:center; gap:8px; margin-bottom:4px; min-width:0; }
767
795
  .ctx-detail .ctx-head b{ font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
768
796
  .ctx-detail .ctx-head small{ color:var(--faint); font-family:var(--mono); font-size:9px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
769
- #connect{ flex:none; border-top:1px solid var(--line); padding:8px; }
770
- #connect-btn{
771
- display:flex; align-items:center; gap:7px; width:100%; justify-content:center;
772
- color:var(--green-mid); border:1px dashed color-mix(in srgb, var(--green) 35%, transparent); border-radius:10px;
773
- padding:7px 10px; font-size:11.5px; transition:box-shadow .15s, background .15s;
774
- }
775
- html:not([data-theme="light"]) #connect-btn:hover{ box-shadow:0 0 12px rgba(61,255,122,.18); }
776
- html[data-theme="light"] #connect-btn:hover{ background:color-mix(in srgb, var(--green) 5%, transparent); }
777
- #connect-btn .plus{ font-size:14px; line-height:1; }
778
- #connect-x{ display:none; margin-top:8px; background:var(--elev); border:1px solid var(--line2); border-radius:10px; padding:10px 11px; }
779
- #connect-x.open{ display:block; }
780
- #connect-x p{ font-size:10.5px; color:var(--dim); line-height:1.5; margin-bottom:7px; }
781
- #connect-x .snip{
782
- font-family:var(--mono); font-size:10px; color:var(--green-mid); background:var(--panel);
783
- border:1px solid var(--line2); border-radius:7px; padding:7px 9px;
784
- white-space:nowrap; overflow-x:auto; margin-bottom:7px;
785
- }
786
- #connect-x .row{ display:flex; justify-content:flex-end; gap:6px; }
787
797
 
788
798
  /* ---- shared kv rows (band detail groups + memory detail internals) ---- */
789
799
  .kv{ display:flex; justify-content:space-between; gap:12px; font-size:11.5px; padding:2.5px 0; min-width:0; }
@@ -975,9 +985,9 @@
975
985
  /* toasts + tooltip */
976
986
  #toasts{ position:fixed; left:50%; bottom:20px; transform:translateX(-50%); z-index:95; display:flex; flex-direction:column; gap:8px; align-items:center; pointer-events:none; }
977
987
 
978
- /* model-settings modal (#422) — the #panel/#connect-x visual language as a
979
- centered overlay; every color rides the theme vars (zero theme-specific
980
- rules, both themes work) */
988
+ /* model-settings modal (#422) — the #panel visual language as a centered
989
+ overlay; every color rides the theme vars (zero theme-specific rules,
990
+ both themes work) */
981
991
  #model-modal{ position:fixed; inset:0; z-index:90; display:grid; place-items:center; }
982
992
  #model-modal[hidden]{ display:none; }
983
993
  #model-modal .mm-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.55); }
@@ -994,6 +1004,61 @@
994
1004
  .mm-check small{ color:var(--faint); }
995
1005
  #model-modal .mm-note{ padding:4px 14px 8px; font-size:10px; color:var(--faint); font-style:italic; }
996
1006
  #model-modal .mm-foot{ display:flex; justify-content:flex-end; gap:7px; padding:10px 14px; border-top:1px solid var(--line); }
1007
+
1008
+ /* plan & billing overlay (#371, owner amendment 2 2026-09-23: the under-
1009
+ graph section was rejected — the upgrade surface must COVER the dash).
1010
+ The #model-modal idiom, widened for two side-by-side option cards; the
1011
+ cover is OPAQUE (var(--bg), not a scrim) — the dash is fully hidden
1012
+ behind it, not dimmed-partial. The [hidden] override is mandatory
1013
+ (author display beats the UA rule — the banner lesson, pinned by tests). */
1014
+ #plan-overlay{ position:fixed; inset:0; z-index:92; display:grid; place-items:center; }
1015
+ #plan-overlay[hidden]{ display:none; }
1016
+ #plan-overlay .po-cover{ position:absolute; inset:0; background:var(--bg); }
1017
+ #plan-overlay .po-card{
1018
+ position:relative; width:min(860px, calc(100vw - 32px)); max-height:calc(100vh - 48px);
1019
+ overflow-y:auto; background:var(--elev); border:1px solid var(--line2); border-radius:14px;
1020
+ box-shadow:var(--shadow-pop);
1021
+ }
1022
+ #plan-overlay .po-head{ display:flex; align-items:center; justify-content:space-between; padding:14px 16px 10px; border-bottom:1px solid var(--line); }
1023
+ #plan-overlay .po-head b{ font-family:var(--serif); font-weight:500; font-size:17px; letter-spacing:.06em; text-transform:uppercase; }
1024
+ #plan-overlay .po-body{ padding:14px 16px 16px; display:flex; flex-direction:column; gap:10px; }
1025
+ #plan-overlay .po-current{ font-size:12.5px; color:var(--dim); }
1026
+ .po-opts{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }
1027
+ #acct-options[hidden]{ display:none; } /* author grid beats [hidden] (renderPlanOverlay hides it when a plan is set) */
1028
+ .po-opt{ background:var(--panel); border:1px solid var(--line2); border-radius:12px; padding:14px 16px 13px; display:flex; flex-direction:column; gap:5px; min-width:0; }
1029
+ .po-opt .po-name{ font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.13em; }
1030
+ .po-opt .po-price{ font-size:26px; font-weight:640; font-variant-numeric:tabular-nums; letter-spacing:.01em; color:var(--green); }
1031
+ html:not([data-theme="light"]) .po-opt .po-price{ text-shadow:0 0 12px rgba(61,255,122,.35); }
1032
+ .po-opt .po-note{ font-size:11px; color:var(--dim); }
1033
+ /* what's included (round 3, owner: "NO info on what is included in the
1034
+ plan") — the marketing site's Cloud feature lines, verbatim, per card. */
1035
+ .po-opt .po-feat{ list-style:none; margin-top:7px; display:flex; flex-direction:column; gap:3px; }
1036
+ .po-opt .po-feat li{ font-size:11px; color:var(--dim); display:flex; gap:7px; align-items:baseline; }
1037
+ .po-opt .po-feat li::before{ content:"✓"; color:var(--green-mid); font-size:10px; }
1038
+ .po-choose{
1039
+ align-self:flex-start; margin-top:8px;
1040
+ color:var(--green-mid); border:1px solid color-mix(in srgb, var(--green) 35%, transparent);
1041
+ border-radius:999px; padding:4px 15px; font-size:11.5px; font-weight:600; white-space:nowrap;
1042
+ }
1043
+ .po-choose:hover{ border-color:var(--green-mid); }
1044
+ .po-choose:disabled{ opacity:.5; cursor:default; }
1045
+ @media (max-width:720px){ .po-opts{ grid-template-columns:1fr; } }
1046
+
1047
+ /* connect overlay (#506) — ONE full-cover surface for every "Connect an
1048
+ agent" entry (the #plan-overlay idiom, owner 2026-09-24: the fleet
1049
+ rail's inline drawer is REMOVED, not kept; the account-menu entry and
1050
+ the Agents-detected pill both open THIS). The cover is OPAQUE
1051
+ (var(--bg), not a scrim) and the [hidden] override is MANDATORY
1052
+ (author display beats the UA rule — the banner lesson, pinned by
1053
+ tests). The snippet blocks reuse the acct-* classes. */
1054
+ #connect-overlay{ position:fixed; inset:0; z-index:92; display:grid; place-items:center; }
1055
+ #connect-overlay[hidden]{ display:none; }
1056
+ #connect-overlay .co-cover{ position:absolute; inset:0; background:var(--bg); }
1057
+ #connect-overlay .co-card{ position:relative; width:min(640px, calc(100vw - 32px)); max-height:calc(100vh - 48px); overflow-y:auto; background:var(--elev); border:1px solid var(--line2); border-radius:14px; box-shadow:var(--shadow-pop); }
1058
+ #connect-overlay .co-head{ display:flex; align-items:center; justify-content:space-between; padding:14px 16px 10px; border-bottom:1px solid var(--line); }
1059
+ #connect-overlay .co-head b{ font-family:var(--serif); font-weight:500; font-size:17px; letter-spacing:.06em; text-transform:uppercase; }
1060
+ #connect-overlay .co-body{ padding:14px 16px 16px; display:flex; flex-direction:column; gap:10px; }
1061
+ #connect-overlay .co-intro{ font-size:12px; color:var(--dim); line-height:1.5; }
997
1062
  .toast{
998
1063
  background:var(--elev); border:1px solid var(--line2); border-radius:999px; padding:8px 17px;
999
1064
  font-size:12px; color:var(--text); box-shadow:var(--shadow-pop);
@@ -1146,6 +1211,11 @@
1146
1211
  </div>
1147
1212
  </header>
1148
1213
 
1214
+ <!-- billing-return banner (#371) — filled by wireBillingBanner() when the
1215
+ URL carries ?billing=success|cancelled (the checkout return URLs the
1216
+ router stamps). Dismiss removes it; the param is stripped on show. -->
1217
+ <div id="billing-banner" role="status" aria-live="polite" hidden></div>
1218
+
1149
1219
  <!-- ============ the stats band — card grid (two rows of four) ============ -->
1150
1220
  <section id="measures" aria-label="Vitals, operator data, and account limits">
1151
1221
  <div class="band" id="strip">
@@ -1164,8 +1234,10 @@
1164
1234
  <div class="csub" id="ag-sub-band">found on their own</div>
1165
1235
  <button class="limit-cta" id="ag-cta">Connect Agent</button>
1166
1236
  </div>
1167
- <!-- #plan-billing: the account menu's "Plan & billing" target — the
1168
- token/cap usage card is this page's billing-adjacent surface -->
1237
+ <!-- #plan-billing: the token/cap usage card — this page's
1238
+ billing-adjacent surface. The account-menu item that scrolled
1239
+ here was consolidated into the Subscription entry (#371 round 3);
1240
+ the anchor id stays (diagnostics + tests pin it). -->
1169
1241
  <div class="bseg c-red" id="plan-billing">
1170
1242
  <div class="gh"><span class="t">Token usage</span></div>
1171
1243
  <div class="big"><span class="n" id="tok-n">—</span><span class="l">used</span><span class="cap-of" id="tok-of" hidden></span></div>
@@ -1284,17 +1356,9 @@
1284
1356
  <aside id="agentscol" aria-label="Connected agents">
1285
1357
  <div class="col-head"><span class="t">Fleet</span><small id="ag-sub">0 &middot; auto-detected</small></div>
1286
1358
  <div id="ag-list"></div>
1287
- <div id="connect">
1288
- <button id="connect-btn"><span class="plus">+</span> connect an agent</button>
1289
- <div id="connect-x">
1290
- <p>Run this on the machine that hosts the agent. It appears here the moment it first talks to the server.</p>
1291
- <div class="snip" id="connect-snip"></div>
1292
- <div class="row">
1293
- <button class="btn sm" id="connect-copy">Copy command</button>
1294
- <button class="btn sm save" id="connect-close">Done</button>
1295
- </div>
1296
- </div>
1297
- </div>
1359
+ <!-- #506: the fleet rail's + connect-an-agent drawer lived here — REMOVED
1360
+ (owner 2026-09-24). Both surviving connect entries (the account-menu
1361
+ entry + the Agents-detected pill) open the #connect-overlay. -->
1298
1362
  </aside>
1299
1363
  </main>
1300
1364
 
@@ -1331,13 +1395,71 @@
1331
1395
  </label>
1332
1396
  <label class="mm-check"><input type="checkbox" id="mm-think"> enable thinking <small>(local endpoints only)</small></label>
1333
1397
  </div>
1334
- <div class="mm-note">applies on server restart</div>
1398
+ <div class="mm-note" id="mm-note">applies on server restart</div>
1335
1399
  <div class="mm-foot">
1336
1400
  <button class="btn" id="mm-cancel">Cancel</button>
1337
1401
  <button class="btn save" id="mm-save">Save</button>
1338
1402
  </div>
1339
1403
  </div>
1340
1404
  </div>
1405
+
1406
+ <!-- ============ the PLAN & BILLING overlay (#371, owner amendment 2
1407
+ 2026-09-23: the under-graph section was rejected after the live look —
1408
+ "the plan and billing needs to open up as overlay, not under the
1409
+ graph, it could not even find it first. needs to cover the normal dash
1410
+ or be a separate tab or view"; the owner picked the full overlay).
1411
+ Opens from the account menu's Subscription entry (round 3: the two
1412
+ billing entries merged into one); the opaque .po-cover fully hides
1413
+ the dash (not dimmed-partial). × / cover-click / ESC close and return
1414
+ where the user was. The option cards are GENERATED from the
1415
+ PLAN_OPTIONS data array (round 3: N-driven — a future plan is a data
1416
+ entry + a router price id, not a page rework); prices and the
1417
+ included-features bullets are display constants aligned with the
1418
+ marketing site (lockstep-pinned by tests). The router deliberately
1419
+ knows only Stripe price ids, so there is no server-side amount to
1420
+ read; the Stripe Checkout page shows the authoritative amount before
1421
+ payment. ============ -->
1422
+ <div id="plan-overlay" hidden>
1423
+ <div class="po-cover" id="po-cover"></div>
1424
+ <div class="po-card" role="dialog" aria-modal="true" aria-labelledby="po-title">
1425
+ <div class="po-head"><b id="po-title">Plan &amp; billing</b><button class="pm-close" id="po-close" aria-label="Close">&#10005;</button></div>
1426
+ <div class="po-body">
1427
+ <div class="po-current" id="po-current"></div>
1428
+ <div class="po-opts" id="acct-options"></div>
1429
+ </div>
1430
+ </div>
1431
+ </div>
1432
+
1433
+ <!-- ============ the CONNECT overlay (#506) — ONE full-cover surface for
1434
+ every "Connect an agent" entry. Before this there were two shapes: the
1435
+ account menu's inline expandable snippets and the fleet rail's +
1436
+ drawer. Owner 2026-09-24: the rail entry is REMOVED, not kept; the
1437
+ account-menu entry and the Agents-detected pill both open THIS
1438
+ (#plan-overlay idiom: opaque cover, × / cover-click / ESC). The
1439
+ snippet <pre>s and copy buttons keep the acct-* ids so
1440
+ renderAcctToken keeps driving them (token-gated: disabled until
1441
+ /account/token answers); the wiring is STATIC so the overlay works
1442
+ even when /account failed and the menu never rendered. The overlay
1443
+ itself lazily fetches the token on open (idempotent). ============ -->
1444
+ <div id="connect-overlay" hidden>
1445
+ <div class="co-cover" id="co-cover"></div>
1446
+ <div class="co-card" role="dialog" aria-modal="true" aria-labelledby="co-title">
1447
+ <div class="co-head"><b id="co-title">Connect an agent</b><button class="pm-close" id="co-close" aria-label="Close">&#10005;</button></div>
1448
+ <div class="co-body">
1449
+ <p class="co-intro">Run this on the machine that hosts the agent. It appears here the moment it first talks to the server.</p>
1450
+ <div class="acct-label">CLI</div>
1451
+ <div class="acct-snippet">
1452
+ <pre id="acct-cli"></pre>
1453
+ <button id="acct-copy-cli" type="button" class="acct-btn" disabled>Copy</button>
1454
+ </div>
1455
+ <div class="acct-label">MCP — ~/.claude.json</div>
1456
+ <div class="acct-snippet">
1457
+ <pre id="acct-mcp"></pre>
1458
+ <button id="acct-copy-mcp" type="button" class="acct-btn" disabled>Copy</button>
1459
+ </div>
1460
+ </div>
1461
+ </div>
1462
+ </div>
1341
1463
  <script>
1342
1464
  'use strict';
1343
1465
  /* =====================================================================
@@ -2292,7 +2414,7 @@ function draw(now, dt){
2292
2414
  if(L.rk>=lodK) continue; // zoom LOD (lines only)
2293
2415
  const a=mems.get(L.a), b=mems.get(L.b);
2294
2416
  if(!a||!b||!aliveAt(a,t)||!aliveAt(b,t)) continue;
2295
- /* #474 final polish: the both-endpoints-in-window gate killed the mesh — links draw between ANY placed memories (bedrock parity: the full LOD-tamed mesh), endpoints carry positions regardless of window membership */
2417
+ /* #474 final polish: the both-endpoints-in-window gate killed the mesh — links draw between ANY placed memories (reference parity: the full LOD-tamed mesh), endpoints carry positions regardless of window membership */
2296
2418
  if(a._emerged===false||b._emerged===false) continue; // thread waits for both endpoints
2297
2419
  const [ax,ay]=bpos(a,now,t), [bx,by]=bpos(b,now,t);
2298
2420
  const ctrl=segCtrlN(ax,ay,bx,by,(L.use>18?1:-1));
@@ -2966,9 +3088,12 @@ document.addEventListener('keydown',e=>{
2966
3088
  else if(e.key==='ArrowRight'){ manualTake(); seek(T+1); }
2967
3089
  else if(e.key==='t'||e.key==='T'){ setTheme(document.documentElement.dataset.theme==='dark'?'light':'dark'); }
2968
3090
  else if(e.key==='Escape'){
2969
- if(document.getElementById('acct-menu') && document.getElementById('acct-menu').classList.contains('open')) closeAcctMenu();
3091
+ const po=el('plan-overlay');
3092
+ const co=el('connect-overlay');
3093
+ if(po && !po.hidden) closePlanOverlay(); /* the full-cover overlay is topmost — it closes first (#371) */
3094
+ else if(co && !co.hidden) closeConnectOverlay(); /* #506 — same tier: the connect overlay */
3095
+ else if(document.getElementById('acct-menu') && document.getElementById('acct-menu').classList.contains('open')) closeAcctMenu();
2970
3096
  else if(panel.classList.contains('open')) closePanel();
2971
- else if(el('connect-x').classList.contains('open')) el('connect-x').classList.remove('open');
2972
3097
  else if(focusDom!=null) setFocus(null);
2973
3098
  }
2974
3099
  wake();
@@ -3273,8 +3398,8 @@ function renderRecBand(ups){
3273
3398
  /* No trend arrow (owner 2026-09-20: removed — unclear). Its renderer and
3274
3399
  the element writes are removed WITH it: a leftover write to a removed
3275
3400
  element is a boot-killing TypeError whenever the recall band carries
3276
- real data (bedrock 0.22.2: "0 memories" — boot died before the counter
3277
- was ever seeded). */
3401
+ real data (a production instance on 0.22.2: "0 memories" — boot died
3402
+ before the counter was ever seeded). */
3278
3403
  }
3279
3404
 
3280
3405
  /* ---------- Memory Precision — Level 1, the pushed index (#476). A compact
@@ -3384,32 +3509,44 @@ function renderModelCard(){
3384
3509
  only as api_key_set), writes PUT with the subset the user touched. The
3385
3510
  api-key input is WRITE-ONLY: prefilled with nothing, sent only when
3386
3511
  non-empty ("leave blank to keep"). enable_thinking is sent only when the
3387
- user toggles the checkbox (tri-state not needed). ---------- */
3512
+ user toggles the checkbox (tri-state not needed).
3513
+ Hosted mode (#514): the GET carries managed:true — the hosting service
3514
+ owns the endpoint + credentials, so their inputs are disabled and never
3515
+ ride the PUT body (a save cannot hit the server's managed-field 403). */
3388
3516
  let mmThinkTouched=false;
3389
3517
  function openModelModal(){
3390
3518
  if(!M) return; // no settings payload → no dialog (degrade)
3391
3519
  const mdl=el('model-modal');
3520
+ const managed=!!M.managed;
3392
3521
  el('mm-backend').value=M.backend||''; // '' = auto / OpenAI-compatible
3393
3522
  el('mm-baseurl').value=M.base_url||'';
3394
3523
  el('mm-model').value=M.model||'';
3395
3524
  const ak=el('mm-apikey'); ak.value='';
3396
- ak.placeholder=M.api_key_set?'configured — leave blank to keep':'api key (optional for local endpoints)';
3525
+ ak.placeholder=managed?'managed by the hosting service'
3526
+ :(M.api_key_set?'configured — leave blank to keep':'api key (optional for local endpoints)');
3397
3527
  el('mm-maxtok').value=M.max_tokens!=null?String(M.max_tokens):'';
3398
3528
  el('mm-think').checked=!!M.enable_thinking;
3399
3529
  mmThinkTouched=false;
3530
+ for(const id of ['mm-backend','mm-baseurl','mm-apikey']) el(id).disabled=managed;
3531
+ el('mm-note').textContent=managed
3532
+ ?'endpoint and credentials are managed by the hosting service — applies on server restart'
3533
+ :'applies on server restart';
3400
3534
  mdl.hidden=false;
3401
- el('mm-baseurl').focus();
3535
+ (managed?el('mm-model'):el('mm-baseurl')).focus();
3402
3536
  }
3403
3537
  function closeModelModal(){ el('model-modal').hidden=true; }
3404
3538
  async function saveModelSettings(){
3405
3539
  /* Build the PUT body from CURRENT field state: empty base_url/model/max
3406
3540
  tokens = clear (null server-side); backend '' clears the named-backend
3407
- key; api_key only rides when typed; enable_thinking only when toggled. */
3541
+ key; api_key only rides when typed; enable_thinking only when toggled.
3542
+ Managed (hosted) mode: backend/base_url/api_key never ride the body. */
3408
3543
  const body={};
3409
- body.backend=el('mm-backend').value;
3410
- const bu=el('mm-baseurl').value.trim(); body.base_url=bu?bu:null;
3544
+ if(!M.managed){
3545
+ body.backend=el('mm-backend').value;
3546
+ const bu=el('mm-baseurl').value.trim(); body.base_url=bu?bu:null;
3547
+ const ak=el('mm-apikey').value; if(ak) body.api_key=ak;
3548
+ }
3411
3549
  const mo=el('mm-model').value.trim(); body.model=mo?mo:null;
3412
- const ak=el('mm-apikey').value; if(ak) body.api_key=ak;
3413
3550
  const mt=el('mm-maxtok').value.trim(); body.max_tokens=mt?Number(mt):null;
3414
3551
  if(mmThinkTouched) body.enable_thinking=el('mm-think').checked;
3415
3552
  try{
@@ -3784,7 +3921,9 @@ function lastNightByAgent(){
3784
3921
  function buildAgents(){
3785
3922
  const listEl=el('ag-list'); listEl.innerHTML='';
3786
3923
  const gs=contextGroups();
3787
- if(!gs.length){ listEl.innerHTML='<div class="pm-empty" style="padding:10px 6px;">no contexts yet — connect an agent below</div>'; return; }
3924
+ /* #506: the "below" target (the rail drawer) is gone — the empty state
3925
+ points at the one remaining connect surface, the account menu */
3926
+ if(!gs.length){ listEl.innerHTML='<div class="pm-empty" style="padding:10px 6px;">no contexts yet — connect an agent from the account menu (top right)</div>'; return; }
3788
3927
  const lastNight=lastNightByAgent();
3789
3928
  /* #423 phase 3: presence + pauses ride D.fleet — ABSENT on a pre-phase-3
3790
3929
  server → no dots, no toggles, no paused state (phase-2 rendering) */
@@ -4022,8 +4161,6 @@ async function refreshFleet(){
4022
4161
  renderBand();
4023
4162
  }catch(e){ toast('Refresh failed: '+e.message); }
4024
4163
  }
4025
- el('connect-btn').onclick=()=>{ el('connect-x').classList.toggle('open'); wake(); };
4026
-
4027
4164
  /* model-settings modal wiring (#422) */
4028
4165
  el('model-cfg').onclick=()=>{ openModelModal(); wake(); };
4029
4166
  el('mm-close').onclick=closeModelModal;
@@ -4031,14 +4168,10 @@ el('mm-cancel').onclick=closeModelModal;
4031
4168
  el('mm-backdrop').onclick=closeModelModal;
4032
4169
  el('mm-save').onclick=()=>{ saveModelSettings(); wake(); };
4033
4170
  el('mm-think').addEventListener('change',()=>{ mmThinkTouched=true; });
4034
- el('ag-cta').onclick=()=>{ el('connect-x').classList.add('open');
4035
- el('connect-btn').scrollIntoView({block:'nearest',behavior:'smooth'}); wake(); };
4036
- el('connect-close').onclick=()=>el('connect-x').classList.remove('open');
4037
- el('connect-snip').textContent='npx @gamaze/hicortex init --server '+location.origin;
4038
- el('connect-copy').onclick=()=>{
4039
- copyToClipboard('npx @gamaze/hicortex init --server '+location.origin, el('connect-copy'));
4040
- wake();
4041
- };
4171
+ /* #506: the Agents-detected pill opens the full-cover connect overlay —
4172
+ the rail drawer (+ button, open/scroll mechanics) it used to target is
4173
+ REMOVED (owner 2026-09-24) */
4174
+ el('ag-cta').onclick=()=>{ openConnectOverlay(); wake(); };
4042
4175
 
4043
4176
  /* ---------- the memory browser — dropdown under the search pill ---------- */
4044
4177
  const panel=el('panel');
@@ -4416,8 +4549,9 @@ async function refreshAfterWrite(){
4416
4549
 
4417
4550
  /* =====================================================================
4418
4551
  ACCOUNT MENU — harvested from the 0.20.4 console (#365): authoritative
4419
- token via lazy GET /account/token, connect snippets, plan & billing,
4420
- sign-out (client-side only — dropping the stored tokens IS the sign-out).
4552
+ token via lazy GET /account/token, connect overlay entry (#506 — the
4553
+ snippets moved to the full-cover overlay), plan & billing, sign-out
4554
+ (client-side only — dropping the stored tokens IS the sign-out).
4421
4555
  ===================================================================== */
4422
4556
  const $ = el; // the harvested block below uses the $ shorthand
4423
4557
  /* Pure decision — what the corner renders for an /account payload (#409 fix
@@ -4441,6 +4575,58 @@ function acctMenuPlan(a) {
4441
4575
  avatar: ((typeof a.name === "string" && a.name) ? a.name : "?").slice(0, 2).toUpperCase(),
4442
4576
  };
4443
4577
  }
4578
+ /* Pure decision (#371) — the purchasable periods. Decided pricing (owner
4579
+ 2026-08-19): ONE price, TWO periods — €12/mo and €108/yr. ANNUAL FIRST:
4580
+ annual is the stated product default; its note is the numeric savings
4581
+ sub-line (round-4 addendum 2026-09-24: it replaces the retired
4582
+ words-only annual tagline — same fact, numbers not words; effective
4583
+ monthly 108/12 = €9, discount (144−108)/144 = 25%, derived-pinned by
4584
+ tests so a price change forces the sub-line update). Prices are
4585
+ display constants — the
4586
+ router deliberately knows only Stripe price ids (no server-side amount
4587
+ exists to read), and the Stripe Checkout page shows the authoritative
4588
+ amount before payment. Round 3: a PURE DATA ARRAY and the single source
4589
+ of truth — the overlay cards are GENERATED from it (renderPlanCards)
4590
+ and the wiring loops it (wireSubscription); no two-plan hardcoding
4591
+ anywhere. The bullets are the marketing site's Cloud feature lines,
4592
+ verbatim (lockstep-pinned against web/index.html) — the seven
4593
+ owner-decided lines (amendment 4, 2026-09-24: copy-first ruling; the
4594
+ agent-cap enforcement rides its own issue); both periods are the same
4595
+ Cloud product, so both carry the same seven. */
4596
+ const PLAN_OPTIONS = [
4597
+ { id: "annual", posts: "annual", name: "Annual", price: "€108/year", note: "€9/mo · 25% off", cta: "Choose annual", bullets: ["Fully managed — no API keys needed", "Claude Code, Hermes, OpenClaw, Pi, OpenCode", "10,000 memories included", "Up to 10 agents", "Learnings & nightly reflection", "Semantic search + knowledge graph", "Multi-machine support"] },
4598
+ { id: "monthly", posts: "monthly", name: "Monthly", price: "€12/mo", note: "", cta: "Choose monthly", bullets: ["Fully managed — no API keys needed", "Claude Code, Hermes, OpenClaw, Pi, OpenCode", "10,000 memories included", "Up to 10 agents", "Learnings & nightly reflection", "Semantic search + knowledge graph", "Multi-machine support"] },
4599
+ ];
4600
+ /* Pure decision (#371) — the Stripe return parameter (?billing=…, the
4601
+ success_url/cancel_url the router stamps on every checkout) → the
4602
+ acknowledgment banner spec, or null when there is nothing to acknowledge
4603
+ (unknown or absent → the page shows nothing, never a guess). */
4604
+ function billingBanner(value) {
4605
+ if (value === "success")
4606
+ return { kind: "success", text: "payment received — activation completes within a minute" };
4607
+ if (value === "cancelled")
4608
+ return { kind: "cancelled", text: "checkout cancelled — nothing was charged" };
4609
+ return null;
4610
+ }
4611
+ /* Pure decision (#371, amendment 2) — the overlay's current-plan line.
4612
+ HONEST by construction: /account provides name/org/plan only, so the
4613
+ line shows the plan label verbatim when set, and when unset the
4614
+ documented meaning of a null label (the same eligibility the Upgrade
4615
+ entry uses) — never invented tiers or numbers. */
4616
+ function acctPlanLine(p) {
4617
+ return p.plan
4618
+ ? "Current plan — " + p.plan
4619
+ : "Current plan — none yet (hosted trial, or self-hosted install)";
4620
+ }
4621
+ /* Pure decision (#371 round 3) — the Subscription entry's Upgrade pill.
4622
+ The menu's two billing entries (Upgrade + Plan & billing) merged into
4623
+ ONE always-rendered "Subscription" item; the PILL is what the old
4624
+ plan-unset eligibility gates (owner: "it can have an upgrade pill/label
4625
+ after it before any subscription is active"). Returns the pill markup
4626
+ or "" — interpolated into the entry's label. */
4627
+ function acctPill(p) {
4628
+ return p.upgrade ? '<span class="acct-pill">Upgrade</span>' : "";
4629
+ }
4444
4630
  function renderAccount(a) {
4445
4631
  const host = $("nav-account");
4446
4632
  const p = acctMenuPlan(a);
@@ -4449,9 +4635,7 @@ function renderAccount(a) {
4449
4635
  const plan = p.plan
4450
4636
  ? `<span class="mini" style="font-size:10.5px;border:1px solid var(--line2);border-radius:999px;padding:2px 9px;color:var(--dim)">${esc(p.plan)}</span>`
4451
4637
  : "";
4452
- const upgrade = p.upgrade
4453
- ? '<button id="acct-upgrade" type="button" class="acct-link acct-upgrade">Upgrade</button>'
4454
- : "";
4638
+ const pill = acctPill(p); // #371 round 3: the entry always renders; the PILL is eligibility-gated
4455
4639
  host.innerHTML = `
4456
4640
  <button id="acct-trigger" type="button" class="acct-trigger" aria-haspopup="menu" aria-expanded="false">
4457
4641
  <span class="avatar">${esc(p.avatar)}</span>${who?'<span class="who">'+who+'</span> ':''}${plan}<span class="acct-caret" aria-hidden="true">&#9660;</span>
@@ -4463,55 +4647,182 @@ function renderAccount(a) {
4463
4647
  <button id="acct-reveal" type="button" class="acct-btn" disabled>Reveal</button>
4464
4648
  <button id="acct-copy-token" type="button" class="acct-btn" disabled>Copy</button>
4465
4649
  </div>
4466
- <details id="acct-connect">
4467
- <summary>Connect an agent</summary>
4468
- <div class="acct-label">CLI</div>
4469
- <div class="acct-snippet">
4470
- <pre id="acct-cli"></pre>
4471
- <button id="acct-copy-cli" type="button" class="acct-btn" disabled>Copy</button>
4472
- </div>
4473
- <div class="acct-label">MCP — ~/.claude.json</div>
4474
- <div class="acct-snippet">
4475
- <pre id="acct-mcp"></pre>
4476
- <button id="acct-copy-mcp" type="button" class="acct-btn" disabled>Copy</button>
4477
- </div>
4478
- </details>
4479
- ${upgrade}
4480
- <button id="acct-plan" type="button" class="acct-link">Plan &amp; billing</button>
4650
+ <button id="acct-connect-entry" type="button" class="acct-link">Connect an agent</button>
4651
+ <button id="acct-subscription" type="button" class="acct-link">Subscription${pill}</button>
4481
4652
  <button id="acct-signout" type="button" class="acct-link acct-danger">Sign out</button>
4482
4653
  </div>`;
4483
4654
  wireAcctMenu();
4484
- if (upgrade) wireUpgrade();
4655
+ renderPlanOverlay(p);
4656
+ wireSubscription();
4657
+ wireConnectEntry();
4658
+ }
4659
+ /* The overlay's content (#371, amendment 2): the honest current-plan line
4660
+ from the pure decision, and the purchase options under the SAME
4661
+ plan-label-unset eligibility as the menu entry (a second checkout for an
4662
+ active subscriber would mint a second Stripe subscription — checkout
4663
+ passes customer_email, not a subscription update; switching is Phase 3).
4664
+ /account failed (p null) → nothing fills (fail-soft, like the empty
4665
+ corner); the overlay itself opens only from the menu entry anyway. */
4666
+ function renderPlanOverlay(p) {
4667
+ if (!p) return;
4668
+ const cur = $("po-current");
4669
+ if (cur) cur.textContent = acctPlanLine(p);
4670
+ const opts = $("acct-options");
4671
+ if (opts) opts.hidden = !p.upgrade;
4672
+ }
4673
+ function openPlanOverlay() {
4674
+ const ov = $("plan-overlay");
4675
+ if (!ov || !ov.hidden) return;
4676
+ ov.hidden = false;
4677
+ const close = $("po-close");
4678
+ if (close) close.focus(); // keyboard users land on the primary exit (the modal focus idiom)
4679
+ }
4680
+ function closePlanOverlay() {
4681
+ const ov = $("plan-overlay");
4682
+ if (ov) ov.hidden = true;
4485
4683
  }
4486
- function wireUpgrade() {
4487
- const btn = $("acct-upgrade");
4684
+ /* #506 — the connect overlay: ONE full-cover surface for every "Connect an
4685
+ agent" entry (the #plan-overlay idiom). Opening lazily fetches the token
4686
+ (idempotent) because the overlay is reachable WITHOUT the account menu
4687
+ (the Agents-detected pill) — the menu path would otherwise be the only
4688
+ thing seeding the snippets. */
4689
+ function openConnectOverlay() {
4690
+ const ov = $("connect-overlay");
4691
+ if (!ov || !ov.hidden) return;
4692
+ ov.hidden = false;
4693
+ fetchAcctToken(); // lazy + idempotent — one attempt per page load
4694
+ const close = $("co-close");
4695
+ if (close) close.focus(); // keyboard users land on the primary exit (the modal focus idiom)
4696
+ }
4697
+ function closeConnectOverlay() {
4698
+ const ov = $("connect-overlay");
4699
+ if (ov) ov.hidden = true;
4700
+ }
4701
+ /* N-driven card render (#371 round 3): the option cards are GENERATED from
4702
+ PLAN_OPTIONS — N entries, N cards, no per-plan literals anywhere in the
4703
+ markup or this template (a future plan is a data entry + a router price
4704
+ id, not a page rework). Runs once at script init; account data only
4705
+ toggles visibility (renderPlanOverlay). */
4706
+ function renderPlanCards() {
4707
+ const host = $("acct-options");
4708
+ if (!host) return;
4709
+ host.innerHTML = PLAN_OPTIONS.map((o) => `
4710
+ <div class="po-opt" id="acct-opt-${o.id}">
4711
+ <div class="po-name">${esc(o.name)}</div>
4712
+ <div class="po-price">${esc(o.price)}</div>
4713
+ ${o.note ? `<div class="po-note">${esc(o.note)}</div>` : ""}
4714
+ <ul class="po-feat">${o.bullets.map((b) => `<li>${esc(b)}</li>`).join("")}</ul>
4715
+ <button id="acct-buy-${o.id}" type="button" class="po-choose">${esc(o.cta)}</button>
4716
+ </div>`).join("");
4717
+ }
4718
+ renderPlanCards();
4719
+ function wireSubscription() {
4720
+ const btn = $("acct-subscription");
4488
4721
  if (!btn) return;
4489
- btn.addEventListener("click", async () => {
4490
- btn.disabled = true;
4491
- try {
4492
- const resp = await fetch("/stripe/checkout", {
4493
- method: "POST",
4494
- headers: { "content-type": "application/json" },
4495
- body: JSON.stringify({ plan: "annual" }),
4496
- });
4497
- if (resp.status === 401) {
4498
- // Never hard-redirect to Google on a daemon (dead end / loop) — only
4499
- // where the probe says OAuth exists; else surface the expired session.
4500
- if (await probeGoogleAuth()) { location.href = "/auth/google/start"; return; }
4501
- alert("Session expired — reload the page to sign in again.");
4502
- btn.disabled = false;
4503
- return;
4504
- }
4505
- const body = await resp.json().catch(() => ({}));
4506
- if (resp.ok && body.url) { location.href = body.url; return; }
4507
- alert("Checkout unavailable (" + resp.status + ") — try again later.");
4508
- btn.disabled = false;
4509
- } catch {
4510
- alert("Checkout unreachable — try again later.");
4722
+ // #371 round 3: the ONE Subscription entry (Upgrade + Plan & billing
4723
+ // merged, owner) opens the FULL-COVER plan & billing overlay; the entry
4724
+ // always renders — the Upgrade pill alone marks the plan-unset state.
4725
+ // Menu closes first; × / cover-click / ESC return.
4726
+ btn.addEventListener("click", () => {
4727
+ closeAcctMenu();
4728
+ openPlanOverlay();
4729
+ });
4730
+ for (const o of PLAN_OPTIONS) {
4731
+ const buy = $("acct-buy-" + o.id);
4732
+ if (!buy) continue;
4733
+ buy.addEventListener("click", () => { startCheckout(o.posts, buy); });
4734
+ }
4735
+ }
4736
+ /* #506: the menu's Connect-an-agent entry opens the full-cover connect
4737
+ overlay — the same launch idiom as wireSubscription (#371): the menu
4738
+ closes first; × / cover-click / ESC close and return where the user
4739
+ was. The inline expandable snippets this entry used to reveal are gone;
4740
+ the overlay owns connecting. */
4741
+ function wireConnectEntry() {
4742
+ const btn = $("acct-connect-entry");
4743
+ if (!btn) return;
4744
+ btn.addEventListener("click", () => {
4745
+ closeAcctMenu();
4746
+ openConnectOverlay();
4747
+ });
4748
+ }
4749
+ /** POST /stripe/checkout for ONE plan, then navigate to the returned Stripe
4750
+ * URL (#371 — each period's action posts its own plan; the hardcoded
4751
+ * annual-only body was the defect). The failure seams are the shipped
4752
+ * checkout-handler contract, kept verbatim for EVERY action: 401 → Google
4753
+ * re-auth where the probe says OAuth exists, else the expired-session
4754
+ * alert; non-ok → the unavailable alert; network error → the unreachable
4755
+ * alert. Each button is disabled independently while its own request is in
4756
+ * flight. */
4757
+ async function startCheckout(plan, btn) {
4758
+ btn.disabled = true;
4759
+ try {
4760
+ const resp = await fetch("/stripe/checkout", {
4761
+ method: "POST",
4762
+ headers: { "content-type": "application/json" },
4763
+ body: JSON.stringify({ plan }),
4764
+ });
4765
+ if (resp.status === 401) {
4766
+ // Never hard-redirect to Google on a daemon (dead end / loop) — only
4767
+ // where the probe says OAuth exists; else surface the expired session.
4768
+ if (await probeGoogleAuth()) { location.href = "/auth/google/start"; return; }
4769
+ alert("Session expired — reload the page to sign in again.");
4511
4770
  btn.disabled = false;
4771
+ return;
4512
4772
  }
4513
- });
4773
+ const body = await resp.json().catch(() => ({}));
4774
+ if (resp.ok && body.url) { location.href = body.url; return; }
4775
+ alert("Checkout unavailable (" + resp.status + ") — try again later.");
4776
+ btn.disabled = false;
4777
+ } catch {
4778
+ alert("Checkout unreachable — try again later.");
4779
+ btn.disabled = false;
4780
+ }
4781
+ }
4782
+ /* The return acknowledgment (#371): read ?billing= once on load and show
4783
+ the one-line banner. The param is STRIPPED via history.replaceState the
4784
+ moment the banner shows — neither dismissal nor a plain reload can
4785
+ re-show it (a reload re-reads a URL that no longer carries the param).
4786
+ Runs at script init, before boot: the acknowledgment must not depend on
4787
+ the data fetches succeeding. */
4788
+ function wireBillingBanner() {
4789
+ const host = $("billing-banner");
4790
+ if (!host) return;
4791
+ const params = new URLSearchParams(location.search);
4792
+ const spec = billingBanner(params.get("billing"));
4793
+ if (!spec) return;
4794
+ params.delete("billing");
4795
+ const qs = params.toString();
4796
+ history.replaceState(null, "", location.pathname + (qs ? "?" + qs : "") + location.hash);
4797
+ host.dataset.kind = spec.kind;
4798
+ host.innerHTML =
4799
+ "<span>" + esc(spec.text) + "</span>" +
4800
+ '<button id="billing-banner-dismiss" type="button" aria-label="Dismiss">&#10005;</button>';
4801
+ host.hidden = false;
4802
+ $("billing-banner-dismiss").addEventListener("click", () => { host.remove(); });
4514
4803
  }
4804
+ wireBillingBanner();
4805
+ /* the plan & billing overlay's close affordances (the #model-modal wiring
4806
+ idiom): the × button and a click on the opaque cover both return to the
4807
+ dash; ESC lives in the shared keyboard handler below. */
4808
+ $("po-close").addEventListener("click", closePlanOverlay);
4809
+ $("po-cover").addEventListener("click", closePlanOverlay);
4810
+ /* the connect overlay's close + copy affordances (#506) — the same static
4811
+ wiring idiom. The copy buttons are STATIC overlay elements, wired at
4812
+ script init so they work even when /account failed and the menu never
4813
+ rendered; the payloads are the same byte-for-byte commands the old
4814
+ handlers copied. Token-gated exactly like the menu's buttons: a copy
4815
+ without the token would paste a command that cannot authenticate. */
4816
+ $("co-close").addEventListener("click", closeConnectOverlay);
4817
+ $("co-cover").addEventListener("click", closeConnectOverlay);
4818
+ const copyCliBtn = $("acct-copy-cli");
4819
+ copyCliBtn.addEventListener("click", () => {
4820
+ if (acctTokenState === "ok") copyToClipboard("npx @gamaze/hicortex init --server " + location.origin, copyCliBtn);
4821
+ });
4822
+ const copyMcpBtn = $("acct-copy-mcp");
4823
+ copyMcpBtn.addEventListener("click", () => {
4824
+ if (acctTokenState === "ok") copyToClipboard(acctMcpText(), copyMcpBtn);
4825
+ });
4515
4826
  let acctToken = null; // token from /account/token (server truth)
4516
4827
  let acctTokenState = "unfetched"; // "unfetched" | "ok" | "unavailable"
4517
4828
  let acctTokenFetched = false; // one lazy fetch attempt per page load
@@ -4539,30 +4850,33 @@ function acctMcpText() {
4539
4850
  }, null, 2);
4540
4851
  }
4541
4852
  function renderAcctToken() {
4853
+ /* #506: per-element guards instead of the old bail-early `if (!val)
4854
+ return` — the snippet <pre>s + copy buttons now live in the STATIC
4855
+ connect overlay, so token state must render even when /account failed
4856
+ and the menu (val/reveal/copy-token) never rendered. Gating semantics
4857
+ are unchanged: everything disabled unless ok, "token unavailable"
4858
+ text otherwise, "…" while unfetched, revealed text only in the ok
4859
+ path. */
4860
+ const ok = acctTokenState === "ok";
4542
4861
  const val = $("acct-token-val");
4543
- if (!val) return; // menu not rendered (/account failed — the corner stays empty)
4544
4862
  const revealBtn = $("acct-reveal");
4545
4863
  const copyToken = $("acct-copy-token");
4546
4864
  const copyCli = $("acct-copy-cli");
4547
4865
  const copyMcp = $("acct-copy-mcp");
4548
- if (acctTokenState !== "ok") {
4549
- val.textContent = acctTokenState === "unfetched" ? "…" : "token unavailable";
4550
- revealBtn.disabled = true;
4551
- copyToken.disabled = true;
4552
- copyCli.disabled = true;
4553
- copyMcp.disabled = true;
4554
- $("acct-cli").textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
4555
- $("acct-mcp").textContent = acctTokenState === "unfetched" ? "" : "token unavailable";
4556
- return;
4557
- }
4558
- revealBtn.disabled = false;
4559
- copyToken.disabled = false;
4560
- copyCli.disabled = false;
4561
- copyMcp.disabled = false;
4562
- val.textContent = acctRevealed ? acctToken : maskToken(acctToken);
4563
- revealBtn.textContent = acctRevealed ? "Hide" : "Reveal";
4564
- $("acct-cli").textContent = acctCliText();
4565
- $("acct-mcp").textContent = acctMcpText();
4866
+ if (revealBtn) revealBtn.disabled = !ok;
4867
+ if (copyToken) copyToken.disabled = !ok;
4868
+ if (copyCli) copyCli.disabled = !ok;
4869
+ if (copyMcp) copyMcp.disabled = !ok;
4870
+ if (val) {
4871
+ if (!ok) val.textContent = acctTokenState === "unfetched" ? "…" : "token unavailable";
4872
+ else val.textContent = acctRevealed ? acctToken : maskToken(acctToken);
4873
+ }
4874
+ if (revealBtn && ok) revealBtn.textContent = acctRevealed ? "Hide" : "Reveal";
4875
+ const cli = $("acct-cli"); // the connect overlay's snippets (#506) — render regardless of menu presence
4876
+ const mcp = $("acct-mcp");
4877
+ const degraded = acctTokenState === "unfetched" ? "" : "token unavailable";
4878
+ if (cli) cli.textContent = ok ? acctCliText() : degraded;
4879
+ if (mcp) mcp.textContent = ok ? acctMcpText() : degraded;
4566
4880
  }
4567
4881
  async function fetchAcctToken() {
4568
4882
  if (acctTokenFetched) return;
@@ -4588,8 +4902,6 @@ function closeAcctMenu() {
4588
4902
  const trigger = document.getElementById("acct-trigger");
4589
4903
  if (trigger) trigger.setAttribute("aria-expanded", "false");
4590
4904
  acctRevealed = false;
4591
- const details = document.getElementById("acct-connect");
4592
- if (details) details.removeAttribute("open");
4593
4905
  renderAcctToken();
4594
4906
  }
4595
4907
  function openAcctMenu() {
@@ -4652,21 +4964,11 @@ function wireAcctMenu() {
4652
4964
  copyToken.addEventListener("click", () => {
4653
4965
  if (acctTokenState === "ok") copyToClipboard(acctConnectLine(), copyToken);
4654
4966
  });
4655
- const copyCli = $("acct-copy-cli");
4656
- copyCli.addEventListener("click", () => {
4657
- if (acctTokenState === "ok") {
4658
- copyToClipboard("npx @gamaze/hicortex init --server " + location.origin, copyCli);
4659
- }
4660
- });
4661
- const copyMcp = $("acct-copy-mcp");
4662
- copyMcp.addEventListener("click", () => {
4663
- if (acctTokenState === "ok") copyToClipboard(acctMcpText(), copyMcp);
4664
- });
4665
- $("acct-plan").addEventListener("click", () => {
4666
- closeAcctMenu();
4667
- const t = document.getElementById("plan-billing");
4668
- if (t) t.scrollIntoView({ behavior: "smooth", block: "nearest" });
4669
- });
4967
+ // #506: the copy-cli/copy-mcp wiring moved to the STATIC init block by
4968
+ // the connect overlay — those buttons live in the overlay now, not here.
4969
+ // #371 round 3: the old "Plan & billing" scroll item is gone — its
4970
+ // target tile (#plan-billing) stays in the measures band as the
4971
+ // billing-adjacent surface; the Subscription entry owns the route now.
4670
4972
  $("acct-signout").addEventListener("click", signOut);
4671
4973
  renderAcctToken();
4672
4974
  }