@worca/app 1.5.0 → 1.6.0-rc.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.
Files changed (101) hide show
  1. package/README.md +7 -1
  2. package/docker/compose.broker.yml +79 -0
  3. package/docker/compose.isolation.yml +40 -0
  4. package/package.json +2 -1
  5. package/src/broker/config.mjs +200 -0
  6. package/src/broker/copilot.mjs +124 -0
  7. package/src/broker/limits.mjs +88 -0
  8. package/src/broker/main.mjs +129 -0
  9. package/src/broker/scrub.mjs +45 -0
  10. package/src/broker/service.mjs +558 -0
  11. package/src/broker/slots.mjs +180 -0
  12. package/src/broker/store.mjs +171 -0
  13. package/src/broker/tokens.mjs +116 -0
  14. package/src/broker/ui/page.css +54 -0
  15. package/src/broker/ui/page.html +25 -0
  16. package/src/broker/ui/page.mjs +202 -0
  17. package/src/broker/ui-server.mjs +217 -0
  18. package/src/broker/usage.mjs +108 -0
  19. package/src/broker/vault.mjs +37 -0
  20. package/src/cli/models.mjs +46 -14
  21. package/src/cli/render.mjs +21 -0
  22. package/src/cli/runs.mjs +296 -0
  23. package/src/cli/worca-cc.mjs +14 -2
  24. package/src/core/agent-pool.mjs +76 -0
  25. package/src/core/artifacts.mjs +40 -8
  26. package/src/core/ask/events.mjs +11 -0
  27. package/src/core/ask/html-text.mjs +113 -0
  28. package/src/core/ask/limits.mjs +5 -0
  29. package/src/core/ask/mcp-stdio.mjs +74 -20
  30. package/src/core/ask/prompt.mjs +25 -6
  31. package/src/core/ask/spawn.mjs +55 -5
  32. package/src/core/ask/store.mjs +1 -1
  33. package/src/core/ask/tools.mjs +66 -1
  34. package/src/core/ask/turn.mjs +52 -3
  35. package/src/core/ask/web-access.mjs +26 -0
  36. package/src/core/ask/web-deps.mjs +56 -0
  37. package/src/core/ask/web-fetch.mjs +271 -0
  38. package/src/core/ask/web-proposal.mjs +76 -0
  39. package/src/core/auto/classify.mjs +20 -8
  40. package/src/core/auto/runnable.mjs +28 -0
  41. package/src/core/billing.mjs +53 -0
  42. package/src/core/bridge/errors.mjs +77 -5
  43. package/src/core/bridge/openrouter.mjs +59 -0
  44. package/src/core/bridge/provider-ops.mjs +165 -9
  45. package/src/core/bridge/providers/endpoint.mjs +112 -7
  46. package/src/core/bridge/registry.mjs +13 -0
  47. package/src/core/bridge/server.mjs +16 -3
  48. package/src/core/bridge/telemetry.mjs +44 -12
  49. package/src/core/bridge/translate/common.mjs +31 -0
  50. package/src/core/bridge/translate/request.mjs +4 -1
  51. package/src/core/bridge/translate/response.mjs +3 -0
  52. package/src/core/bridge/translate/schema-keywords.mjs +75 -0
  53. package/src/core/bridge/translate/stream.mjs +50 -4
  54. package/src/core/bridge/upstream.mjs +102 -17
  55. package/src/core/broker-boot.mjs +57 -0
  56. package/src/core/broker-client.mjs +206 -0
  57. package/src/core/broker-guard.mjs +112 -0
  58. package/src/core/broker-routing.mjs +138 -0
  59. package/src/core/claude-auth.mjs +29 -0
  60. package/src/core/claude-runner.mjs +200 -9
  61. package/src/core/config.mjs +9 -12
  62. package/src/core/failure-policy.mjs +4 -2
  63. package/src/core/git-info.mjs +49 -5
  64. package/src/core/github-credentials.mjs +44 -1
  65. package/src/core/graph/script-runner.mjs +3 -1
  66. package/src/core/list-prices.mjs +29 -0
  67. package/src/core/mcp-secrets.mjs +80 -0
  68. package/src/core/metrics/sync.mjs +2 -1
  69. package/src/core/model-env.mjs +72 -0
  70. package/src/core/model-test.mjs +17 -5
  71. package/src/core/onboarding.mjs +12 -6
  72. package/src/core/openrouter-free.mjs +159 -0
  73. package/src/core/orchestrator.mjs +100 -12
  74. package/src/core/policy/effective.mjs +18 -1
  75. package/src/core/policy/local.mjs +4 -1
  76. package/src/core/policy/registry.mjs +12 -2
  77. package/src/core/preflight.mjs +116 -0
  78. package/src/core/recoverable-error.mjs +95 -0
  79. package/src/core/recovery-backoff.mjs +84 -0
  80. package/src/core/redact.mjs +25 -0
  81. package/src/core/run-context.mjs +6 -0
  82. package/src/core/run-harness.mjs +112 -30
  83. package/src/core/run-report.mjs +2 -1
  84. package/src/core/settings.mjs +90 -5
  85. package/src/core/title.mjs +7 -2
  86. package/src/core/web-allowlist.mjs +95 -0
  87. package/ui/public/app.js +244 -43
  88. package/ui/public/ask-model.mjs +1 -0
  89. package/ui/public/ask-panel.mjs +160 -23
  90. package/ui/public/bridge-view.mjs +175 -6
  91. package/ui/public/chat-settings-view.mjs +52 -1
  92. package/ui/public/credential-badges.mjs +63 -0
  93. package/ui/public/credentials-view.mjs +57 -0
  94. package/ui/public/index.html +33 -6
  95. package/ui/public/models-view.mjs +19 -1
  96. package/ui/public/openrouter-free-view.mjs +118 -0
  97. package/ui/public/stats-view.mjs +56 -0
  98. package/ui/public/style.css +99 -50
  99. package/ui/public/team-policy-view.mjs +2 -1
  100. package/ui/public/ws-seq.mjs +24 -0
  101. package/ui/server.mjs +319 -23
@@ -70,8 +70,8 @@
70
70
  --trunc-ink:light-dark(#994466,#E39BBE);
71
71
  --chip-ink:light-dark(#555555,#B4B4AE);
72
72
 
73
- --r-card:24px;
74
- --r-ctrl:14px;
73
+ --r-card:14px;
74
+ --r-ctrl:10px;
75
75
  --shadow:0 6px 28px light-dark(rgba(25,25,27,.05),rgba(0,0,0,.45)), 0 1px 2px light-dark(rgba(25,25,27,.04),rgba(0,0,0,.35));
76
76
  --shadow-soft:0 2px 12px light-dark(rgba(25,25,27,.04),rgba(0,0,0,.35));
77
77
  --shadow-pop:0 6px 28px light-dark(rgba(25,25,27,.10),rgba(0,0,0,.55)),0 1px 2px light-dark(rgba(25,25,27,.06),rgba(0,0,0,.4));
@@ -503,7 +503,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
503
503
 
504
504
  /* @-mention popup: attached-file completions anchored under the prompt caret */
505
505
  .mention-popup{position:fixed;z-index:70;min-width:180px;max-width:320px;max-height:210px;overflow-y:auto;
506
- background:var(--panel);border:1px solid var(--line-2);border-radius:12px;box-shadow:var(--shadow);padding:5px;}
506
+ background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-card);box-shadow:var(--shadow);padding:5px;}
507
507
  .mention-item{display:block;width:100%;text-align:left;border:0;background:transparent;color:var(--ink-2);
508
508
  font-family:inherit;font-weight:500;font-size:12.5px;padding:7px 10px;border-radius:8px;cursor:pointer;
509
509
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
@@ -622,7 +622,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
622
622
  One collapsed row per agent showing its effective config; the controls only
623
623
  exist once a row is opened, so the page height no longer grows with the
624
624
  workflow. (newpipeline-ux-design.md §4.2) */
625
- .agents-cfg{margin:6px 0 18px;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--field);}
625
+ .agents-cfg{margin:6px 0 18px;border:1px solid var(--line);border-radius:var(--r-ctrl);overflow:hidden;background:var(--field);}
626
626
  .agents-head{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--line);}
627
627
  .agents-title{font-weight:700;font-size:13px;}
628
628
  .agents-wf{flex:0 0 auto;font-size:11.5px;font-weight:600;color:var(--ink-2);
@@ -727,7 +727,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
727
727
  .btn-clear{background:var(--field);border:none;border-radius:10px;padding:8px 15px;font-family:inherit;
728
728
  font-weight:600;font-size:12.5px;cursor:pointer;color:var(--ink-2);transition:.15s;}
729
729
  .btn-clear:hover{background:var(--line-2);}
730
- .log{background:var(--panel);border:1px solid var(--line);border-radius:16px;min-height:260px;max-height:380px;overflow-y:auto;
730
+ .log{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);min-height:260px;max-height:380px;overflow-y:auto;
731
731
  padding:18px 20px;font-family:var(--mono);font-size:12.5px;line-height:1.85;color:var(--ink-2);
732
732
  white-space:pre-wrap;word-break:break-word;}
733
733
  .log .t{color:var(--ink-3);}
@@ -762,7 +762,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
762
762
  (design D14). Amber family; the circle inverts to --panel so it still reads
763
763
  against the pale wash. */
764
764
  .run-ask-banner{display:flex;align-items:center;gap:12px;margin:0 0 18px;padding:13px 18px;
765
- background:var(--amber-wash);border:1px solid var(--amber);border-radius:16px;}
765
+ background:var(--amber-wash);border:1px solid var(--amber);border-radius:var(--r-card);}
766
766
  .run-ask-banner[hidden]{display:none;} /* author display:flex beats the UA [hidden] rule */
767
767
  .run-ask-banner .rab-mark{flex:0 0 auto;display:flex;align-items:center;justify-content:center;
768
768
  width:26px;height:26px;border-radius:50%;background:var(--panel);
@@ -773,7 +773,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
773
773
  everything, amber like the other "needs you" banners. */
774
774
  .session-expired{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:10000;
775
775
  display:flex;align-items:center;gap:14px;max-width:calc(100vw - 32px);padding:10px 12px 10px 18px;
776
- background:var(--amber-wash);border:1px solid var(--amber);border-radius:14px;
776
+ background:var(--amber-wash);border:1px solid var(--amber);border-radius:var(--r-card);
777
777
  box-shadow:var(--shadow-pop);}
778
778
  .session-expired .se-text{font-weight:500;font-size:13px;color:var(--amber-ink);}
779
779
  .session-expired .se-reload{font:inherit;font-size:13px;font-weight:600;padding:6px 14px;border-radius:10px;
@@ -866,7 +866,7 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
866
866
  first — keeping the prose free of them means a future sweep that forgets to
867
867
  strip still passes. */
868
868
  .qpanel{margin-top:18px;padding:14px 16px 13px;background:var(--amber-wash);
869
- border:1px solid var(--amber-bg);border-radius:14px;}
869
+ border:1px solid var(--amber-bg);border-radius:var(--r-ctrl);}
870
870
  .qpanel-head{display:flex;align-items:center;gap:11px;}
871
871
  .qpanel-head svg{stroke:var(--amber-ink);flex:0 0 auto;}
872
872
  .qpanel-head b{flex:1;min-width:0;font-weight:600;font-size:12.5px;color:var(--amber-ink);}
@@ -1237,7 +1237,7 @@ button.af-gal-card:focus-visible{outline:2px solid var(--ink);outline-offset:2px
1237
1237
  .hist-head .h-meta b{cursor:pointer;}
1238
1238
  .hist-head .h-meta b:hover{text-decoration:underline;}
1239
1239
  .retained-banner{margin:0 0 18px;padding:16px 18px;border:1.5px solid var(--amber);
1240
- border-radius:14px;background:var(--amber-bg);color:var(--amber-ink);font-size:12.5px;line-height:1.55;}
1240
+ border-radius:var(--r-card);background:var(--amber-bg);color:var(--amber-ink);font-size:12.5px;line-height:1.55;}
1241
1241
  .retained-banner[hidden]{display:none;}
1242
1242
  .retained-banner h4{margin:0 0 6px;font-size:14px;color:var(--amber-ink);}
1243
1243
  .retained-banner p{margin:5px 0;}
@@ -1273,7 +1273,7 @@ button.af-gal-card:focus-visible{outline:2px solid var(--ink);outline-offset:2px
1273
1273
 
1274
1274
  /* ---------- Saved-markdown viewer ---------- */
1275
1275
  .viewer{font-family:var(--mono);font-size:12.5px;line-height:1.7;background:var(--canvas-2);border:1px solid var(--line);
1276
- border-radius:16px;padding:18px 20px;max-height:480px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;}
1276
+ border-radius:var(--r-card);padding:18px 20px;max-height:480px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;}
1277
1277
 
1278
1278
  /* ---------- scrollbars ---------- */
1279
1279
  ::-webkit-scrollbar{width:10px;height:10px;}
@@ -1334,7 +1334,7 @@ button.af-gal-card:focus-visible{outline:2px solid var(--ink);outline-offset:2px
1334
1334
  .topnav{display:none;}
1335
1335
  @media (max-width:1080px){
1336
1336
  .topnav{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:20px;background:var(--panel);
1337
- border:1px solid var(--line);border-radius:14px;padding:5px;}
1337
+ border:1px solid var(--line);border-radius:var(--r-card);padding:5px;}
1338
1338
  .topnav button{flex:1;text-align:center;padding:9px 12px;border:0;border-radius:10px;
1339
1339
  background:transparent;color:var(--ink-2);font-family:inherit;font-weight:600;font-size:13px;
1340
1340
  cursor:pointer;transition:.15s;}
@@ -1562,6 +1562,10 @@ button.wd-member:focus-visible{outline:2px solid var(--ink);outline-offset:-2px;
1562
1562
  .wiz-proj:hover{background:var(--surface);}
1563
1563
  .wiz-proj input{margin:0;cursor:pointer;}
1564
1564
  .wiz-proj.missing{color:var(--ink-3);cursor:not-allowed;}
1565
+ .wiz-proj-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;}
1566
+ .wiz-select-all{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--ink-2);cursor:pointer;}
1567
+ .wiz-select-all input{margin:0;cursor:pointer;}
1568
+ .wiz-select-all:has(input:disabled){opacity:.5;cursor:not-allowed;}
1565
1569
 
1566
1570
  /* Live scan loader (role=status aria-live=polite). */
1567
1571
  .ws-loader{display:flex;flex-direction:column;align-items:center;gap:12px;padding:36px 24px;text-align:center;}
@@ -1595,7 +1599,7 @@ button.wd-member:focus-visible{outline:2px solid var(--ink);outline-offset:-2px;
1595
1599
  /* ties `.gv-world .node` and loses on source order — silently killing the v2 */
1596
1600
  /* status border colours. */
1597
1601
  /* ========================================================================== */
1598
- .run-flow-wrap{position:relative;margin:0 0 18px;border-radius:18px;border:1px solid var(--line);overflow-x:auto;overflow-y:hidden;background-color:var(--canvas-2);background-image:radial-gradient(circle, var(--line-2) 1.1px, transparent 1.1px);background-size:22px 22px;}
1602
+ .run-flow-wrap{position:relative;margin:0 0 18px;border-radius:var(--r-card);border:1px solid var(--line);overflow-x:auto;overflow-y:hidden;background-color:var(--canvas-2);background-image:radial-gradient(circle, var(--line-2) 1.1px, transparent 1.1px);background-size:22px 22px;}
1599
1603
  .run-flow{position:relative;display:flex;align-items:center;gap:44px;padding:66px 30px 52px;width:max-content;min-width:100%;}
1600
1604
  /* Status badge (top-right pip). */
1601
1605
  .run-flow .nstat{position:absolute;top:-8px;right:-8px;width:21px;height:21px;border-radius:50%;display:grid;place-items:center;box-shadow:var(--shadow-soft);z-index:3;}
@@ -1874,7 +1878,7 @@ input.bench-case-name{width:100%;padding:9px 12px;font-size:13px;}
1874
1878
  .wz-pick-line{font-size:14px;color:var(--ink-2);}
1875
1879
  .wz-rt-cards{display:flex;gap:20px;width:100%;max-width:1080px;}
1876
1880
  @media (max-width:900px){.wz-rt-cards{flex-direction:column;}}
1877
- .wz .rt{display:flex;flex-direction:column;gap:12px;flex:1 1 0;min-width:0;min-height:250px;padding:22px;border-radius:20px;background:var(--panel);border:2px solid transparent;box-shadow:var(--shadow);cursor:pointer;text-align:left;font-family:inherit;color:var(--ink);}
1881
+ .wz .rt{display:flex;flex-direction:column;gap:12px;flex:1 1 0;min-width:0;min-height:250px;padding:22px;border-radius:var(--r-card);background:var(--panel);border:2px solid transparent;box-shadow:var(--shadow);cursor:pointer;text-align:left;font-family:inherit;color:var(--ink);}
1878
1882
  .wz .rt:hover:not(:disabled){transform:translateY(-1px);}
1879
1883
  .wz .rt.sel{border-color:var(--ink);}
1880
1884
  .wz .rt:disabled{opacity:.45;cursor:not-allowed;}
@@ -1890,7 +1894,7 @@ input.bench-case-name{width:100%;padding:9px 12px;font-size:13px;}
1890
1894
  .wz .panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);}
1891
1895
  .wz-panel-title{font-size:13.5px;font-weight:600;}
1892
1896
  .wz-tile{display:flex;align-items:center;gap:14px;padding:14px 16px;}
1893
- .wz .tile{width:48px;height:48px;border-radius:14px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;}
1897
+ .wz .tile{width:48px;height:48px;border-radius:var(--r-ctrl);display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;}
1894
1898
  .wz .tile-green{background:var(--green-bg);color:var(--green-ink);}.wz .tile-peach{background:var(--peach-bg);color:var(--peach-ink);}
1895
1899
  .wz .tile-red{background:var(--red-bg);color:var(--red-ink);}.wz .tile-blue{background:var(--blue-bg);color:var(--blue-ink);}
1896
1900
  .wz .tile-violet{background:var(--violet-bg);color:var(--violet-ink);}.wz .tile-amber{background:var(--amber-bg);color:var(--amber-ink);}
@@ -2262,6 +2266,21 @@ button.pd-ov-card:hover{border-color:var(--ink-3);}
2262
2266
  without this the rows touch while every other gap is 14px. */
2263
2267
  .charts-grid + .charts-grid{margin-top:14px;}
2264
2268
  .chart-card{padding:20px 22px;}
2269
+ /* Stats › By person (credential broker): one full-width card, a table with a share bar. */
2270
+ .people-grid{grid-template-columns:1fr;}
2271
+ .people-scroll{overflow-x:auto;}
2272
+ .people-table{width:100%;border-collapse:collapse;font-size:13px;font-variant-numeric:tabular-nums;}
2273
+ .people-table th{text-align:left;font-weight:500;color:var(--ink-3);font-size:12px;padding:6px 10px 6px 0;border-bottom:1px solid var(--line);white-space:nowrap;}
2274
+ .people-table td{padding:8px 10px 8px 0;border-bottom:1px solid var(--line);vertical-align:middle;}
2275
+ .people-table tr:last-child td{border-bottom:none;}
2276
+ .people-table td.num{white-space:nowrap;color:var(--ink-2);}
2277
+ .people-name{overflow-wrap:anywhere;}
2278
+ .people-spend{min-width:160px;}
2279
+ .people-usd{display:inline-block;min-width:64px;font-weight:600;}
2280
+ .people-bar{display:inline-block;vertical-align:middle;width:90px;height:6px;border-radius:3px;background:var(--line);margin-left:8px;overflow:hidden;}
2281
+ .people-bar-fill{display:block;height:100%;background:var(--blue);}
2282
+ .people-slots{color:var(--ink-2);}
2283
+ .people-card > .hint{display:block;margin-top:10px;}
2265
2284
  .chart-fig{margin:0;}
2266
2285
  .chart-svg{display:block;width:100%;height:auto;}
2267
2286
  .chart-empty{display:grid;place-items:center;min-height:180px;}
@@ -2291,7 +2310,7 @@ button.pd-ov-card:hover{border-color:var(--ink-3);}
2291
2310
  .tm-sync-error{white-space:pre-wrap;margin:0;}
2292
2311
  section[data-view="team-metrics"] .topbar{align-items:flex-start;}
2293
2312
  .sync-chip{margin-left:auto;display:flex;align-items:center;}
2294
- .sync-chip-inner{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:8px 12px;border:1px solid var(--line-2);border-radius:14px;background:var(--surface);font-size:12.5px;max-width:520px;}
2313
+ .sync-chip-inner{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:8px 12px;border:1px solid var(--line-2);border-radius:var(--r-card);background:var(--surface);font-size:12.5px;max-width:520px;}
2295
2314
  .sync-chip-inner .dot.green{background:var(--green);} .sync-chip-inner .dot.amber{background:var(--amber);} .sync-chip-inner .dot.red{background:var(--red);}
2296
2315
  .sync-chip-inner .tm-sync-error{flex-basis:100%;color:var(--red-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
2297
2316
  .tm-filter{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:18px;}
@@ -2451,7 +2470,7 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2451
2470
  .tl-lg-sw .tl-span,.tl-lg-sw .tl-run{height:12px;margin-top:-6px;}
2452
2471
  .tl-foot{display:block;color:var(--ink-2);}
2453
2472
 
2454
- .tl-pop{position:fixed;z-index:60;width:340px;max-width:calc(100vw - 32px);background:var(--panel);border:1px solid var(--line);border-radius:20px;
2473
+ .tl-pop{position:fixed;z-index:60;width:340px;max-width:calc(100vw - 32px);background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);
2455
2474
  box-shadow:var(--shadow-sheet);padding:16px 18px;display:flex;flex-direction:column;gap:10px;}
2456
2475
  .tl-pop h3{margin:0;font-size:16px;font-weight:600;line-height:1.3;text-wrap:balance;}
2457
2476
  .tl-pop-top{display:flex;align-items:center;justify-content:space-between;gap:8px;}
@@ -2504,7 +2523,7 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2504
2523
  .tm-dot.green{background:var(--green);} .tm-dot.amber{background:var(--amber);} .tm-dot.red{background:var(--red);} .tm-dot.grey{background:var(--ink-3);}
2505
2524
  /* Enable dialog (boards 5/6) */
2506
2525
  /* `.field > label` (display:block, margin-bottom) would otherwise beat this and stack the radio above the title */
2507
- .radio-card,.field > label.radio-card{display:flex;gap:12px;align-items:flex-start;padding:12px 14px;border:1.5px solid var(--line-2);border-radius:var(--r-ctrl,14px);margin:8px 0 0;cursor:pointer;font-weight:500;font-size:13px;}
2526
+ .radio-card,.field > label.radio-card{display:flex;gap:12px;align-items:flex-start;padding:12px 14px;border:1.5px solid var(--line-2);border-radius:var(--r-ctrl,10px);margin:8px 0 0;cursor:pointer;font-weight:500;font-size:13px;}
2508
2527
  .radio-card input[type="radio"]{flex:0 0 auto;margin:2px 0 0;}
2509
2528
  /* :has(), not a render-time .on class: changing the Attribution radio does not re-render the body */
2510
2529
  .radio-card:has(> input:checked){border-color:var(--ink);background:var(--field);}
@@ -2516,7 +2535,7 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2516
2535
  .wiz-track{display:flex;gap:18px;list-style:none;padding:0;margin:0 0 18px;font-size:12.5px;color:var(--ink-2);}
2517
2536
  .wiz-track li span{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:50%;background:var(--field);margin-right:6px;font-weight:700;}
2518
2537
  .wiz-track li.on{color:var(--ink);font-weight:600;} .wiz-track li.on span{background:var(--ink);color:var(--on-ink);}
2519
- .wiz-list{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:14px;}
2538
+ .wiz-list{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:var(--r-ctrl);}
2520
2539
  .wiz-row{display:flex;align-items:center;gap:14px;padding:12px 14px;border-top:1px solid var(--line);}
2521
2540
  /* The metrics-home picker's radio + slug: the label had no rule, so the radio touched the text. */
2522
2541
  .wiz-row-pick{display:flex;align-items:center;gap:10px;min-width:0;cursor:pointer;}
@@ -2590,6 +2609,8 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2590
2609
  fill (--line), under verify:theme's 4.5:1. A loss keeps neutral ink (--red-ink is
2591
2610
  4.07:1 there); its "−" carries it. */
2592
2611
  .spend-ind-saved{margin-top:6px;}
2612
+ /* OpenRouter free-model requests (openrouter-free-view.mjs): a second block under the spend block. */
2613
+ .free-ind{margin-top:8px;}
2593
2614
  .spend-ind-saved.pos .spend-ind-label,.spend-ind-saved.pos .spend-ind-amt{color:var(--green-ink-strong);}
2594
2615
  .topnav-amt{margin-left:7px;font-family:var(--mono);font-weight:600;}
2595
2616
  .topnav-amt.warn{color:var(--amber-ink);}
@@ -2600,6 +2621,22 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2600
2621
  /* One width/centering/gap rule for every Settings card. */
2601
2622
  .settings-card{max-width:760px;padding:22px;margin-left:auto;margin-right:auto;}
2602
2623
  .settings-card + .settings-card{margin-top:18px;}
2624
+ .settings-card[hidden]{display:none;}
2625
+
2626
+ /* My model credentials (credential broker): one row per slot, a state chip on the right. */
2627
+ .cred-list{list-style:none;margin:0;padding:0;display:grid;gap:8px;}
2628
+ .cred-row{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:13px;}
2629
+ .cred-name{min-width:0;overflow-wrap:anywhere;}
2630
+ .cred-chip{font-size:12px;font-weight:600;padding:3px 9px;border-radius:999px;white-space:nowrap;}
2631
+ .cred-chip.tone-green{background:var(--green-bg);color:var(--green-ink);}
2632
+ .cred-chip.tone-red{background:var(--red-bg);color:var(--red-ink);}
2633
+ .cred-chip.tone-blue{background:var(--blue-bg);color:var(--blue-ink);}
2634
+ .cred-chip.tone-slate{background:var(--slate-bg);color:var(--slate-ink);}
2635
+ .cred-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:14px;}
2636
+ /* Providers card with the credential broker: keys, sign-in and key tests live on the key page. */
2637
+ .mv-providers--broker .mv-cp-signin,.mv-providers--broker .mv-cp-signout,.mv-providers--broker .mv-cp-quota-refresh,
2638
+ .mv-providers--broker .mv-cp-quota,.mv-providers--broker .mv-pv-test,.mv-providers--broker .mv-field:has(.mv-pv-key){display:none;}
2639
+ .mv-broker-note{display:block;margin:6px 0 12px;}
2603
2640
 
2604
2641
  /* About card rows. .about-val and .about-link share specificity on one anchor, so
2605
2642
  .about-link must stay AFTER .about-val (test/ui-settings-about.test.mjs pins it). */
@@ -2640,6 +2677,9 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2640
2677
  .pause-error-banner .peb-text{color:var(--ink);font-family:var(--mono);font-size:12px;word-break:break-word;}
2641
2678
  .pause-error-banner .peb-hint{color:var(--ink-2);margin-top:6px;}
2642
2679
  .hd-banners .pause-error-banner,.rd-banners .pause-error-banner{align-self:stretch;margin:0;}
2680
+ /* In a run card these banners are nested boxes (--r-ctrl); in the page-level banner
2681
+ stacks they sit on .retained-banner, so they take its card radius. */
2682
+ .hd-banners .cost-banner,.hd-banners .pause-error-banner,.rd-banners .cost-banner,.rd-banners .pause-error-banner{border-radius:var(--r-card);}
2643
2683
 
2644
2684
  @media (max-width:1080px){
2645
2685
  .stat-row{grid-template-columns:repeat(2,1fr);}
@@ -2724,6 +2764,9 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2724
2764
  bridge's copy uses --ink-2 (6.5:1) so the theme proof passes without a new entry. */
2725
2765
  .mv-providers .hint,.mv-conn .hint,.mv-degradation,.mvi .hint,.mv-cp-signin-box .hint{color:var(--ink-2);}
2726
2766
  .mv-pv-row .input::placeholder,.mv-conn .input::placeholder,.mv-conn .textarea::placeholder{color:var(--ink-2);}
2767
+ /* Settings → Ask Worca → Web access: its fields are new, so their placeholders use --ink-2 like the rule
2768
+ above instead of joining the legacy light contrast baseline (the allowlist textarea failed at 2.58:1). */
2769
+ .ask-web .input::placeholder{color:var(--ink-2);}
2727
2770
  .mv-providers .mv-providers-hint{margin:-6px 0 0;}
2728
2771
  .mv-pv-row{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:12px 18px;padding:14px 0 0;border-top:1px solid var(--line-2);}
2729
2772
  .mv-pv-main{display:grid;gap:6px;align-content:start;min-width:0;}
@@ -2782,12 +2825,12 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2782
2825
  .mv-section-head{display:flex;align-items:center;gap:8px;}
2783
2826
  .mv-sec-toggle{display:flex;align-items:center;gap:8px;padding:2px 0;border:0;background:transparent;cursor:pointer;color:inherit;font:inherit;}
2784
2827
  .mv-sec-toggle .mv-section-title{margin:0;}
2785
- .mv-sec-caret{color:var(--ink-2);font-size:11px;line-height:1;transition:transform .12s ease;}
2828
+ .mv-sec-caret{flex:0 0 auto;transform:rotate(90deg);} /* .adv-chev: points right; a group is open unless folded */
2786
2829
  .mv-sec-count{padding:1px 8px;border-radius:999px;background:var(--field);color:var(--ink-2);font:600 11px var(--mono);}
2787
- .mv-section.is-folded .mv-sec-caret{transform:rotate(-90deg);}
2830
+ .mv-section.is-folded .mv-sec-caret{transform:none;}
2788
2831
  .mv-section.is-folded .mv-sec-body,.mv-section.is-folded .mv-sec-hint{display:none;}
2789
2832
  .mv-no-hits{margin-top:6px;}
2790
- @media (prefers-reduced-motion: reduce){.mv-sec-caret{transition:none;}}
2833
+ @media (prefers-reduced-motion: reduce){.mv-sec-caret,.advanced .adv-chev{transition:none;}}
2791
2834
  .mv-pv-btns .btn-ghost,.mv-pv-btns .btn-go{padding:7px 12px;font-size:12px;}
2792
2835
  .mv-pv-row .input,.mv-pv-row .select{font-family:var(--mono);font-size:12.5px;padding:9px 12px;}
2793
2836
  .mv-pv-row .mv-pv-conc{max-width:120px;}
@@ -2826,6 +2869,10 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2826
2869
  .mv-conn-note{margin:0;}
2827
2870
  .mvi-tbl{margin-top:8px;}
2828
2871
  .mvi-tbl td{vertical-align:top;}
2872
+ /* A hosted catalog's filters (OpenRouter lists hundreds): the shared .chip-select pills, one row. */
2873
+ .mvi-or-filters{margin-top:10px;}
2874
+ .mvi-or-filters .mvi-f-ctx{padding:2px 6px;font-size:12.5px;min-height:0;}
2875
+ .mv-pv-presets{display:block;margin-top:4px;}
2829
2876
  .mvi-tbl .mvi-id{display:block;margin:2px 0 0;font-family:var(--mono);}
2830
2877
  .mvi-tbl tr.mvi-disabled{opacity:.55;}
2831
2878
  .mvi-status{color:var(--ink-3);font-size:12px;}
@@ -2968,7 +3015,7 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2968
3015
  .hd-more:hover,.hd-more[aria-expanded="true"]{background:var(--field);border-color:var(--ink-3);}
2969
3016
  .hd-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:5;min-width:214px;
2970
3017
  display:flex;flex-direction:column;gap:2px;padding:6px;background:var(--panel);
2971
- border:1px solid var(--line-2);border-radius:16px;box-shadow:var(--shadow-menu);}
3018
+ border:1px solid var(--line-2);border-radius:var(--r-card);box-shadow:var(--shadow-menu);}
2972
3019
  .hd-menu[hidden]{display:none;}
2973
3020
  /* Both items shed their pill dressing in here and become full-width rows. Higher
2974
3021
  specificity than the standalone rules above, which still hold nowhere else. */
@@ -3147,7 +3194,7 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
3147
3194
  reply row draws its own elbow, exactly like `.nav-child` under Running. */
3148
3195
  .hd-cmt-thread{position:relative;display:flex;flex-direction:column;max-width:720px;
3149
3196
  animation:wr-rise .18s cubic-bezier(.2,.7,.3,1) both;}
3150
- .hd-cmt-card{background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow-soft);}
3197
+ .hd-cmt-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-card);box-shadow:var(--shadow-soft);}
3151
3198
  .hd-cmt-block>.hd-cmt-card{max-width:720px;}
3152
3199
  .hd-cmt-head{display:flex;align-items:center;flex-wrap:wrap;gap:6px 8px;min-height:20px;
3153
3200
  padding:12px 14px 0;font-size:11px;color:var(--ink-2);}
@@ -3443,7 +3490,7 @@ button.hd-mem-chip:focus-visible{outline:2px solid var(--ink);outline-offset:2px
3443
3490
  view's, and `.run-log .log` keeps its own sizes there (D7 — the Running view is
3444
3491
  untouched). Neither rule sets `display`, so `.hd-sec[hidden]` (:1703) still hides
3445
3492
  the whole panel and no `[hidden]` twin is needed. */
3446
- .hd-sec-logs .log{background:var(--panel);border:1px solid var(--line);border-radius:16px;
3493
+ .hd-sec-logs .log{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);
3447
3494
  min-height:260px;max-height:520px;overflow-y:auto;padding:18px 20px;}
3448
3495
  .hd-sec-logs .log-filters{margin-bottom:10px;}
3449
3496
 
@@ -3481,7 +3528,7 @@ button.hd-mem-chip:focus-visible{outline:2px solid var(--ink);outline-offset:2px
3481
3528
  .shipit-card h2{margin:18px 0 0;font:700 20px/1.25 var(--sans);letter-spacing:-.02em;}
3482
3529
  .shipit-sub{margin:8px 0 0;font:400 13.5px/1.6 var(--sans);color:var(--ink-2);}
3483
3530
  .shipit-summary{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;
3484
- margin-top:18px;padding:14px;background:var(--field);border-radius:14px;font-size:12px;color:var(--ink-2);}
3531
+ margin-top:18px;padding:14px;background:var(--field);border-radius:var(--r-ctrl);font-size:12px;color:var(--ink-2);}
3485
3532
  .shipit-summary[hidden]{display:none;}
3486
3533
  /* An empty flex child still eats a `gap`, so a results-less run ("+12 −3", no file
3487
3534
  count) would render a stray 8px hole. Hide the value spans by NAME — a blanket
@@ -3798,7 +3845,7 @@ body.view-running .topnav{margin:12px 32px 0;flex:0 0 auto;}
3798
3845
 
3799
3846
  /* ---------- Running detail: pipeline graph ---------- */
3800
3847
  /* The dot-grid panel spec §5.3 asks for is already `.run-flow-wrap` —
3801
- radial-gradient var(--line-2) 1.1px at 22px 22px, 18px radius, horizontal
3848
+ radial-gradient var(--line-2) 1.1px at 22px 22px, --r-card radius, horizontal
3802
3849
  scroll. This only supplies the margin box; the wrap's own legacy
3803
3850
  `margin:0 0 18px` is zeroed so it does not double up with .rd-graph's. */
3804
3851
  .rd-graph{margin-top:18px;}
@@ -3870,12 +3917,12 @@ body.view-running .topnav{margin:12px 32px 0;flex:0 0 auto;}
3870
3917
  .rd-sec[hidden]{display:none;}
3871
3918
  .rd-sec:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
3872
3919
 
3873
- /* Live log tab (§5.6): white pane, --line border, 16px radius, bounded height.
3920
+ /* Live log tab (§5.6): white pane, --line border, --r-card radius, bounded height.
3874
3921
  The pane is the shared `.log` markup inside a `.run-log` block, so `.run-log .log`
3875
3922
  is (0,2,0) and so is this — source order is what makes it win, which is why the
3876
3923
  rule lives at the END of the file and must not be moved above it. */
3877
3924
  .rd-sec-logs .log{min-height:300px;max-height:520px;
3878
- background:var(--panel);border:1px solid var(--line);border-radius:16px;}
3925
+ background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);}
3879
3926
  /* §9's fourth keyframe: the blinking green caret at the tail of a LIVE log.
3880
3927
  CSS-only (a ::after on the pane) so no JS has to keep an element last after
3881
3928
  every append; Task 9 stamps `.rd-terminal` on the screen when the run ends and
@@ -3984,7 +4031,7 @@ body.view-running .topnav{margin:12px 32px 0;flex:0 0 auto;}
3984
4031
  .stop-card{padding:24px;animation:wr-rise .28s cubic-bezier(.2,.7,.3,1) both;}
3985
4032
  .stop-title{margin:0 0 12px;font:700 17px/1.25 var(--sans);letter-spacing:-.015em;}
3986
4033
  .stop-body{margin:0;font:400 13.5px/1.6 var(--sans);color:var(--ink-2);}
3987
- .stop-ident{margin-top:14px;padding:11px 13px;background:var(--field);border-radius:14px;
4034
+ .stop-ident{margin-top:14px;padding:11px 13px;background:var(--field);border-radius:var(--r-ctrl);
3988
4035
  font:400 12px/1.6 var(--mono);color:var(--ink-2);word-break:break-all;}
3989
4036
  /* author display rule beats the UA [hidden] rule — a branch-less run must not
3990
4037
  leave a blank mono line in the block */
@@ -4363,7 +4410,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4363
4410
  attribute from its ledger of rows the transcript has shown before, and on
4364
4411
  nothing else. */
4365
4412
  .ask-msg[data-ask-enter]{animation:wr-rise .2s cubic-bezier(.2,.7,.3,1) both;}
4366
- .ask-user-bubble{align-self:flex-end;max-width:78%;padding:10px 14px;border-radius:16px 16px 4px 16px;
4413
+ .ask-user-bubble{align-self:flex-end;max-width:78%;padding:10px 14px;border-radius:var(--r-card) var(--r-card) 4px var(--r-card);
4367
4414
  background:var(--field);color:var(--ink);font-size:13.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;}
4368
4415
  .ask-user-pills{justify-content:flex-end;margin-top:0;}
4369
4416
  .ask-attachment-pill{padding-right:12px;}
@@ -4461,7 +4508,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4461
4508
  .ask-error-line{font-size:12.5px;color:var(--red-ink);}
4462
4509
 
4463
4510
  .ask-card{margin-top:2px;display:flex;flex-direction:column;gap:10px;padding:13px 15px;
4464
- border:1.5px solid var(--line);border-radius:16px;background:var(--panel);}
4511
+ border:1.5px solid var(--line);border-radius:var(--r-card);background:var(--panel);}
4465
4512
  .ask-card-seg{display:flex;gap:4px;}
4466
4513
  .ask-card-seg-btn{padding:5px 12px;border:1px solid var(--line-2);border-radius:999px;background:transparent;
4467
4514
  color:var(--ink-2);font-family:inherit;font-weight:600;font-size:11px;cursor:pointer;transition:.15s;}
@@ -4493,7 +4540,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4493
4540
  /* ---------- run proposal card v2 (ui/public/ask-panel.mjs buildCardForm; mockup 2026-09-06) ----------
4494
4541
  The v1 card rules above still apply to this root (.ask-card); every override below
4495
4542
  is written at higher specificity than the rule it replaces. Tokens only. */
4496
- .ask-card.ask-rp{gap:0;padding:0;overflow:hidden;border-radius:18px;border:1.5px solid var(--line-2);box-shadow:var(--shadow-soft);animation:wr-rise .26s cubic-bezier(.2,.7,.3,1) both;}
4543
+ .ask-card.ask-rp{gap:0;padding:0;overflow:hidden;border-radius:var(--r-card);border:1.5px solid var(--line-2);box-shadow:var(--shadow-soft);animation:wr-rise .26s cubic-bezier(.2,.7,.3,1) both;}
4497
4544
  .ask-rp>*{min-width:0;}
4498
4545
  .ask-rp-head{padding:16px 18px 14px;background:linear-gradient(180deg,var(--violet-wash),var(--panel) 92%);border-bottom:1px solid var(--line);}
4499
4546
  .ask-rp-eyebrow{display:flex;align-items:center;gap:8px;height:20px;}
@@ -4523,7 +4570,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4523
4570
  .ask-rp .ask-rp-title input:focus-visible{box-shadow:none;outline:none;border-bottom-color:var(--violet);background:transparent;}
4524
4571
  .ask-rp-wfdesc{font-size:11.5px;color:var(--ink-3);}
4525
4572
  .ask-rp-wfdesc b{font-weight:600;color:var(--ink-2);}
4526
- .ask-rp-agents{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--canvas-2);}
4573
+ .ask-rp-agents{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:var(--r-ctrl);overflow:hidden;background:var(--canvas-2);}
4527
4574
  .ask-rp-tile{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-top:1px solid var(--line);background:var(--panel);}
4528
4575
  .ask-rp-tile:first-child{border-top:0;}
4529
4576
  .ask-rp-tile.mod{background:var(--amber-wash);}
@@ -4572,7 +4619,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4572
4619
  .ask-rp-foot .ask-card-start svg{width:11px;height:11px;}
4573
4620
  .ask-rp .ask-card-err{padding:0 18px 10px;}
4574
4621
  .ask-rp .ask-card-scope-warn{margin:12px 18px 0;}
4575
- .ask-pop-at{width:288px;padding:6px;border-radius:14px;max-height:min(320px,60%);overflow-y:auto;}
4622
+ .ask-pop-at{width:288px;padding:6px;border-radius:var(--r-card);max-height:min(320px,60%);overflow-y:auto;}
4576
4623
  @media (max-width:640px){.ask-rp-grid,.ask-rp-grid.two{grid-template-columns:1fr;}}
4577
4624
  @media (prefers-reduced-motion: reduce){.ask-card.ask-rp{animation:none;} .ask-rp *{transition:none!important;}}
4578
4625
 
@@ -4731,7 +4778,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4731
4778
  the contrast baseline (scripts/verify-theme-cdp.mjs) it has today. */
4732
4779
  .ask-composer{position:relative;flex:0 0 auto;padding:10px 16px 14px;display:flex;flex-direction:column;}
4733
4780
  .ask-composer-box{width:100%;max-width:var(--ask-col-max);margin-inline:auto;padding:8px 10px;
4734
- border:1px solid var(--line-2);border-radius:16px;background:var(--panel);
4781
+ border:1px solid var(--line-2);border-radius:var(--r-card);background:var(--panel);
4735
4782
  display:flex;flex-direction:column;gap:4px;}
4736
4783
  .ask-chips{display:flex;flex-wrap:wrap;gap:6px;padding:2px 4px 4px;}
4737
4784
  .ask-chips[hidden]{display:none;}
@@ -4776,19 +4823,19 @@ body.rail-collapsed .ask-dock{left:76px;}
4776
4823
  .ask-wt-btn[hidden]{display:none;}
4777
4824
 
4778
4825
  .ask-pop{position:absolute;z-index:3;background:var(--panel);border:1px solid var(--line-2);
4779
- padding:6px;box-shadow:var(--shadow-menu);border-radius:16px;}
4826
+ padding:6px;box-shadow:var(--shadow-menu);border-radius:var(--r-card);}
4780
4827
  .ask-pop-threads{top:46px;right:76px;width:326px;}
4781
4828
  /* The composer popovers hug the box (--ask-col-inset, see .ask-sheet) and float
4782
4829
  just above it (--ask-box-top); agents/worktrees keep their old offsets left of
4783
4830
  the model popover. */
4784
4831
  .ask-pop-scope{left:var(--ask-col-inset);bottom:calc(var(--ask-box-top) + 6px);width:300px;max-height:min(420px,70%);overflow-y:auto;}
4785
- .ask-pop-model{right:var(--ask-col-inset);bottom:calc(var(--ask-box-top) + 6px);width:292px;border-radius:18px;}
4832
+ .ask-pop-model{right:var(--ask-col-inset);bottom:calc(var(--ask-box-top) + 6px);width:292px;border-radius:var(--r-card);}
4786
4833
  .ask-pop-runinfo{right:calc(var(--ask-col-inset) + 66px);bottom:calc(var(--ask-box-top) + 6px);width:326px;z-index:4;}
4787
4834
  /* The chip picker (ask-panel.mjs openChipPicker) is the only JS-positioned panel: a band
4788
4835
  chip sits wherever the transcript scrolled it. Its rows are the WHOLE model catalog, so
4789
4836
  the menu outgrows the sheet, which clips -- the same cap .ask-pop-scope carries keeps
4790
4837
  the Effort row reachable, and chipPickerTop() flips it above the chip when that fits. */
4791
- .ask-pop-chip{width:288px;padding:6px;border-radius:14px;max-height:min(420px,70%);overflow-y:auto;}
4838
+ .ask-pop-chip{width:288px;padding:6px;border-radius:var(--r-card);max-height:min(420px,70%);overflow-y:auto;}
4792
4839
  .ask-pop-caption,.ask-pop-caption-row .ask-pop-caption{padding:8px 10px 6px;font-weight:600;font-size:10px;
4793
4840
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);}
4794
4841
  .ask-pop-caption-row{display:flex;align-items:baseline;gap:8px;}
@@ -4817,6 +4864,7 @@ body.rail-collapsed .ask-dock{left:76px;}
4817
4864
  border-radius:999px;white-space:nowrap;background:var(--field);color:var(--ink-3);}
4818
4865
  .ask-model-tag.is-warn{flex:0 0 auto;max-width:none;background:var(--amber-bg);color:var(--amber-ink);}
4819
4866
  .ask-model-tag.is-err{flex:0 0 auto;max-width:none;background:var(--red-bg);color:var(--red-ink);}
4867
+ .ask-model-tag.is-key{flex:0 0 auto;max-width:none;background:var(--green-bg);color:var(--green-ink);}
4820
4868
  .ask-model-item .ask-model-name{flex:0 1 auto;min-width:4.5ch;overflow:hidden;text-overflow:ellipsis;
4821
4869
  white-space:nowrap;}
4822
4870
  /* The threads list is the only scrollport in a popover: /api/ask/threads returns up
@@ -5244,6 +5292,7 @@ html{scrollbar-color:var(--line-2) transparent;scrollbar-width:thin;}
5244
5292
  .ob-claude-cmd{margin:12px 0;padding:12px 14px;background:var(--field);border-radius:12px;overflow:auto;}
5245
5293
  .ob-claude-cmd code{background:none;padding:0;font-size:12.5px;}
5246
5294
  .ob-claude-note a{color:var(--blue-ink-strong);}
5295
+ .ob-claude-step{margin-top:16px;}
5247
5296
  .ob-claude-status{min-height:18px;margin-top:12px;font-size:13px;line-height:1.45;}
5248
5297
  .ob-claude-status.ok{color:var(--green-ink);}
5249
5298
  .ob-claude-status.err{color:var(--red-ink);}
@@ -5270,14 +5319,14 @@ html{scrollbar-color:var(--line-2) transparent;scrollbar-width:thin;}
5270
5319
  .select-wrap.guide-lift::after{z-index:46;}
5271
5320
  /* The ring never fades — it is the answer to "which control?". The pulse is a
5272
5321
  halo radiating OUT of it, so the mark stays solid and the motion draws the eye. */
5273
- .guide-ring{position:fixed;z-index:46;pointer-events:none;border:2.5px solid var(--violet);border-radius:18px;
5322
+ .guide-ring{position:fixed;z-index:46;pointer-events:none;border:2.5px solid var(--violet);border-radius:calc(var(--r-ctrl) + 6px);
5274
5323
  animation:guide-pulse 1.8s ease-out infinite;}
5275
5324
  @keyframes guide-pulse{
5276
5325
  0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--violet) 55%,transparent);}
5277
5326
  100%{box-shadow:0 0 0 14px color-mix(in srgb,var(--violet) 0%,transparent);}
5278
5327
  }
5279
5328
  .guide-balloon{position:fixed;z-index:47;display:flex;flex-direction:column;align-items:flex-start;gap:6px;
5280
- max-width:300px;padding:11px 14px 10px;background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-ctrl);
5329
+ max-width:300px;padding:11px 14px 10px;background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-card);
5281
5330
  box-shadow:var(--shadow-pop);color:var(--ink);font-size:13px;line-height:1.45;}
5282
5331
  .guide-balloon::before{content:'';position:absolute;top:-6px;left:calc(var(--arrow-x,20px) - 5px);width:10px;height:10px;
5283
5332
  background:var(--panel);border-left:1px solid var(--line-2);border-top:1px solid var(--line-2);transform:rotate(45deg);}
@@ -5329,7 +5378,7 @@ html{scrollbar-color:var(--line-2) transparent;scrollbar-width:thin;}
5329
5378
  .btn-split > .btn-split-more:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
5330
5379
  .btn-split > .btn-go:disabled,.btn-split > .btn-split-more:disabled{opacity:.45;cursor:not-allowed;transform:none;filter:none;}
5331
5380
  .btn-split-menu{position:absolute;right:0;bottom:calc(100% + 8px);z-index:20;min-width:230px;padding:6px;
5332
- background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow-menu);}
5381
+ background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);box-shadow:var(--shadow-menu);}
5333
5382
  .btn-split-menu[hidden]{display:none;}
5334
5383
  .btn-split-menu button{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;text-align:left;
5335
5384
  background:transparent;border:none;border-radius:11px;padding:10px 12px;font-family:inherit;cursor:pointer;color:var(--ink);}
@@ -5516,7 +5565,7 @@ a.ask-card-task-title:hover{text-decoration-color:currentColor;}
5516
5565
 
5517
5566
  /* Page-above-the-mode banner. */
5518
5567
  .level-banner{display:flex;flex-wrap:wrap;align-items:center;gap:10px 12px;margin:0 0 18px;
5519
- padding:10px 14px;border-radius:var(--r-ctrl);background:var(--canvas-2);border:1px solid var(--line-2);
5568
+ padding:10px 14px;border-radius:var(--r-card);background:var(--canvas-2);border:1px solid var(--line-2);
5520
5569
  font-size:13px;color:var(--ink-2);}
5521
5570
  .level-banner[hidden]{display:none;}
5522
5571
  .level-banner-text{flex:1 1 220px;min-width:0;}
@@ -5554,7 +5603,7 @@ body.view-workspaces .level-banner{margin:12px 32px 0;flex:0 0 auto;}
5554
5603
  .mode-sub{margin:0 0 16px;font-size:13px;line-height:1.55;color:var(--ink-2);}
5555
5604
  .lv-cards{display:flex;flex-direction:column;gap:8px;}
5556
5605
  .lv-card{display:grid;grid-template-columns:40px minmax(0,1fr) 20px;align-items:center;gap:14px;width:100%;
5557
- padding:13px 15px;border:1.5px solid var(--line-2);border-radius:16px;background:var(--panel);color:var(--ink);
5606
+ padding:13px 15px;border:1.5px solid var(--line-2);border-radius:var(--r-ctrl);background:var(--panel);color:var(--ink);
5558
5607
  font:inherit;text-align:left;cursor:pointer;transition:border-color .15s,background .15s;}
5559
5608
  .lv-card:hover{background:var(--field);}
5560
5609
  .lv-card:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
@@ -5637,7 +5686,7 @@ body.view-workspaces .level-banner{margin:12px 32px 0;flex:0 0 auto;}
5637
5686
  .gv-wires path.ghost.legal{stroke:var(--green);stroke-dasharray:none;}
5638
5687
  .gv-wires path.ghost.illegal{stroke:var(--red);}
5639
5688
  .gv-world .node{position:absolute;left:0;top:0;display:block;padding:0;gap:0;align-items:stretch;z-index:2;
5640
- width:var(--gv-node-w);background:var(--panel);border:var(--gv-border) solid var(--line-2);border-radius:calc(16px * var(--gv-scale));
5689
+ width:var(--gv-node-w);background:var(--panel);border:var(--gv-border) solid var(--line-2);border-radius:calc(14px * var(--gv-scale));
5641
5690
  box-shadow:var(--shadow-soft);cursor:grab;transition:box-shadow .12s;}
5642
5691
  .gv-world .node::before{content:none;}
5643
5692
  .gv-world .node:hover{box-shadow:var(--shadow);}
@@ -5645,7 +5694,7 @@ body.view-workspaces .level-banner{margin:12px 32px 0;flex:0 0 auto;}
5645
5694
  .gv-world .node.dragging{cursor:grabbing;transition:none;}
5646
5695
  .gv-static .node,.gv-static .wbadge{pointer-events:none;}
5647
5696
  .gv-world .nhead{height:var(--gv-head-h);display:flex;align-items:center;gap:calc(8px * var(--gv-scale));padding:0 calc(11px * var(--gv-scale));
5648
- border-radius:calc(14.5px * var(--gv-scale)) calc(14.5px * var(--gv-scale)) 0 0;font-size:max(9px,calc(13px * var(--gv-scale)));font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
5697
+ border-radius:calc(12.5px * var(--gv-scale)) calc(12.5px * var(--gv-scale)) 0 0;font-size:max(9px,calc(13px * var(--gv-scale)));font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
5649
5698
  .gv-world .nhead svg{width:calc(15px * var(--gv-scale));height:calc(15px * var(--gv-scale));flex:0 0 auto;stroke-width:1.85;}
5650
5699
  .gv-world .nhead .tt{overflow:hidden;text-overflow:ellipsis;}
5651
5700
  .gv-world .nhead .chip.rt{margin-left:auto;flex:0 0 auto;font:700 max(8px,calc(10px * var(--gv-scale)))/1.6 var(--mono);text-transform:none;white-space:nowrap;
@@ -5740,7 +5789,7 @@ body.view-workspaces .level-banner{margin:12px 32px 0;flex:0 0 auto;}
5740
5789
  lines), and test/ui-graph-css.test.mjs asserts style.css uses the FULL --gv-*
5741
5790
  set injectGeometry writes — dropping them fails that assertion. */
5742
5791
  .gv-world .xfoot{position:absolute;left:0;right:0;bottom:0;border-top:1px solid var(--line);
5743
- border-radius:0 0 14.5px 14.5px;overflow:hidden;background:var(--panel);}
5792
+ border-radius:0 0 12.5px 12.5px;overflow:hidden;background:var(--panel);}
5744
5793
  .gv-world .xfoot>.fan,.gv-world .xfoot>.xtoggle,.gv-world .xfoot>.xresult,.gv-world .xfoot>.xlive{height:var(--gv-exec-row-h);
5745
5794
  display:flex;align-items:center;gap:7px;padding:0 11px;width:100%;box-sizing:border-box;}
5746
5795
  .gv-world .xfoot>:first-child{height:var(--gv-foot-h);} /* the first line is the tall one nodeSize bills */
@@ -6087,7 +6136,7 @@ input.gv-name:focus-visible{outline:none;}
6087
6136
  .gv-archived .pl-chip:hover{border-color:var(--red);color:var(--red-ink);background:var(--red-bg);}
6088
6137
 
6089
6138
  /* ---------- save dialog ---------- */
6090
- .save-dialog{border:none;border-radius:18px;padding:0;box-shadow:var(--shadow);max-width:420px;width:92vw;}
6139
+ .save-dialog{border:none;border-radius:var(--r-card);padding:0;box-shadow:var(--shadow);max-width:420px;width:92vw;}
6091
6140
  .save-dialog::backdrop{background:var(--scrim-soft);}
6092
6141
  .save-dialog .sd-body{padding:20px 22px 18px;display:flex;flex-direction:column;gap:14px;}
6093
6142
  .save-dialog .sd-title{margin:0;font-size:16px;font-weight:700;letter-spacing:-.015em;}
@@ -6181,7 +6230,7 @@ input.gv-name:focus-visible{outline:none;}
6181
6230
  /* History: node click drives the Logs tab */
6182
6231
  .hd-graph.linked .gv-world .node[data-node-id]{cursor:pointer;}
6183
6232
  /* quiescence banner (.rd-banners / .hd-banners / History Overview note) */
6184
- .run-warn{margin:0;padding:11px 15px;border-radius:12px;background:var(--amber-bg);color:var(--amber-ink);font-size:12.5px;font-weight:600;border:1px solid color-mix(in srgb, var(--amber) 45%, transparent);align-self:stretch;}
6233
+ .run-warn{margin:0;padding:11px 15px;border-radius:var(--r-card);background:var(--amber-bg);color:var(--amber-ink);font-size:12.5px;font-weight:600;border:1px solid color-mix(in srgb, var(--amber) 45%, transparent);align-self:stretch;}
6185
6234
  .run-warn[hidden]{display:none;}
6186
6235
  /* gesture hint chip (D10) */
6187
6236
  .rg-hint{position:absolute;left:12px;bottom:10px;padding:4px 9px;border-radius:999px;background:color-mix(in srgb, var(--ink) 72%, transparent);color:var(--on-ink);font-size:10.5px;font-weight:600;pointer-events:none;opacity:0;transition:opacity .15s;z-index:5;}
@@ -6472,7 +6521,7 @@ input.gv-name:focus-visible{outline:none;}
6472
6521
  /* The catalog editor: one bordered item per set / model, its fields on a grid. */
6473
6522
  .tp-cat-list{display:flex;flex-direction:column;gap:12px;}
6474
6523
  .tp-cat-list:empty::before{content:'None yet.';color:var(--ink-3);font-size:12.5px;}
6475
- .tp-cat-item{display:flex;flex-direction:column;gap:12px;padding:14px 16px;border:1px solid var(--line);border-radius:14px;background:var(--surface);}
6524
+ .tp-cat-item{display:flex;flex-direction:column;gap:12px;padding:14px 16px;border:1px solid var(--line);border-radius:var(--r-ctrl);background:var(--surface);}
6476
6525
  .tp-cat-item-head{display:flex;align-items:center;gap:10px;font-size:13.5px;}
6477
6526
  .tp-cat-item-head .tp-cat-rm{margin-left:auto;padding:5px 11px;font-size:12px;}
6478
6527
  .tp-cat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px 14px;}
@@ -6534,7 +6583,7 @@ input.gv-name:focus-visible{outline:none;}
6534
6583
  /* The pure editor's bar (team-policy-view.mjs): the page dismantles it into the header. */
6535
6584
  .tp-publish-bar{display:flex;align-items:center;gap:12px;}
6536
6585
  .tp-publish-bar .grow{flex:1;font-size:12.5px;color:var(--ink-2);}
6537
- .tp-json{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px;font-size:12px;line-height:1.5;overflow-x:auto;margin:0;}
6586
+ .tp-json{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-card);padding:14px 16px;font-size:12px;line-height:1.5;overflow-x:auto;margin:0;}
6538
6587
  .tp-msg{white-space:pre-wrap;}
6539
6588
  @media (max-width:900px){ .tp-edit-row{grid-template-columns:1fr;} }
6540
6589
  /* workspace card line (board 6) */
@@ -696,6 +696,7 @@ function valueControl(doc, meta, entry, row) {
696
696
  case 'bool': {
697
697
  const lab = h(doc, 'label', 'switch-row'); const cb = h(doc, 'input', 'sw-input tp-val'); cb.type = 'checkbox'; cb.checked = v === true;
698
698
  cb.setAttribute('aria-label', meta.label);
699
+ if (meta.offOnly) cb.disabled = true; // a policy can only switch it off (registry: offOnly)
699
700
  lab.append(cb, h(doc, 'span', 'switch switch-sm'), h(doc, 'span', 'txt', v === true ? 'on' : 'off'));
700
701
  cb.addEventListener('change', () => { lab.querySelector('.txt').textContent = cb.checked ? 'on' : 'off'; });
701
702
  wrap.append(lab); break;
@@ -713,7 +714,7 @@ function valueControl(doc, meta, entry, row) {
713
714
  case 'string[]': {
714
715
  writeItems(row, Array.isArray(v) ? v : []);
715
716
  wrap.append(h(doc, 'div', 'tp-list'));
716
- const add = h(doc, 'div', 'path-row tp-add-row'); const inp = h(doc, 'input', 'input tp-add'); inp.type = 'text'; inp.placeholder = meta.key === 'plugins.marketplaces' ? 'owner/repo or URL' : meta.key === 'models.allowed' ? 'model id' : 'plugin name';
717
+ const add = h(doc, 'div', 'path-row tp-add-row'); const inp = h(doc, 'input', 'input tp-add'); inp.type = 'text'; inp.placeholder = meta.key === 'plugins.marketplaces' ? 'owner/repo or URL' : meta.key === 'models.allowed' ? 'model id' : meta.key === 'ask.webAllowedDomains' ? 'example.com or *.example.com' : 'plugin name';
717
718
  const b = h(doc, 'button', 'btn btn-ghost btn-mini tp-add-btn', '+ add'); b.type = 'button';
718
719
  add.append(inp, b); wrap.append(add);
719
720
  break;
@@ -0,0 +1,24 @@
1
+ // ui/public/ws-seq.mjs
2
+ // A reconnect re-subscribes to a live run and the server replays that run's buffered events
3
+ // (ui/server.mjs replayEntry). Each buffered event carries a per-run `seq`; one this page has
4
+ // already applied is skipped, so a socket that drops (a proxy's idle timeout) never shows a
5
+ // log line twice. Frames without a seq (state snapshots, other channels) always apply.
6
+ // Pure: no DOM.
7
+
8
+ /** Whether `msg` was already applied to run `r`; records it when it was not. */
9
+ export function alreadyApplied(r, msg) {
10
+ const seq = msg && typeof msg.seq === 'number' ? msg.seq : null;
11
+ if (seq === null || !r) return false;
12
+ if (seq <= (r.lastSeq || 0)) return true;
13
+ r.lastSeq = seq;
14
+ return false;
15
+ }
16
+
17
+ /** A restarted server numbers from 1 again: forget what was applied when its boot id changes. */
18
+ export function noteBoot(state, bootId, runsMap) {
19
+ if (!bootId) return;
20
+ if (state.serverBootId && state.serverBootId !== bootId) {
21
+ for (const r of runsMap.values()) r.lastSeq = 0;
22
+ }
23
+ state.serverBootId = bootId;
24
+ }