dsh-claude-style 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -17,6 +17,8 @@
17
17
  * - src/overrides/permissions.js
18
18
  * - src/overrides/model-brand.js
19
19
  * - src/overrides/model-picker.js
20
+ * - src/overrides/hero-menu.js
21
+ * - src/overrides/quick-providers.js
20
22
  * - src/overrides/account-footer.js
21
23
  * - src/overrides/ban-screen.js
22
24
  * - src/overrides/theme-flip.js
@@ -36,6 +38,7 @@
36
38
  * - src/styles/components/account-footer.css
37
39
  * - src/styles/components/ban-screen.css
38
40
  * - src/styles/components/model-picker.css
41
+ * - src/styles/components/hero-menu.css
39
42
  * - src/styles/components/footer-takeover.css
40
43
  * - src/styles/components/third-party.css
41
44
  * - src/styles/components/settings.css
@@ -220,6 +223,14 @@ window.__ModuleLoader__.load({
220
223
  var DEFAULT_BAN_LOCALE = BAN_LOCALE_EN
221
224
  /** Present while the composer restyle applies to the page currently shown. */
222
225
  var COMPOSER_ATTR = 'data-dsh-claude-composer-active'
226
+ /**
227
+ * Stamped on the host's shared menu card while it is the hero row's picker
228
+ * (the workspace chip or the agent-preset seat opened it). The host portals
229
+ * that card to <body> with no marker of its own, so the stylesheet cannot
230
+ * tell it from the host's other menus; src/overrides/hero-menu.js stamps it
231
+ * and components/hero-menu.css switches on this attribute.
232
+ */
233
+ var HERO_MENU_ATTR = 'data-dsh-claude-hero-menu'
223
234
  /**
224
235
  * Present while the browser window does NOT hold focus.
225
236
  *
@@ -231,6 +242,16 @@ window.__ModuleLoader__.load({
231
242
  var WINDOW_BLUR_ATTR = 'data-dsh-window-blur'
232
243
  /** Composer surfaces the restyle may cover, in settings order. */
233
244
  var COMPOSER_SCOPES = ['off', 'hero', 'conversation', 'all']
245
+ /**
246
+ * How eagerly the skin's popovers open on hover: `off` is click-only,
247
+ * `account` auto-opens the sidebar account popover alone, and `all` adds the
248
+ * permission, model, session-stats and the host's two hero-row pickers.
249
+ */
250
+ var AUTO_POPOVER_OFF = 'off'
251
+ var AUTO_POPOVER_ACCOUNT = 'account'
252
+ var AUTO_POPOVER_ALL = 'all'
253
+ var AUTO_POPOVER_SCOPES = [AUTO_POPOVER_OFF, AUTO_POPOVER_ACCOUNT, AUTO_POPOVER_ALL]
254
+ var DEFAULT_AUTO_POPOVER = AUTO_POPOVER_ALL
234
255
  /** Route the browser half reads and writes preferences through (lib/index.js). */
235
256
  var PREFS_ROUTE = '/dsh-claude-style/prefs'
236
257
  /** Route that resolves the host OS user once; never polled. */
@@ -1888,21 +1909,22 @@ window.__ModuleLoader__.load({
1888
1909
  " content below, roomy type. */",
1889
1910
  "body[data-dsh-claude-style][data-dsh-claude-composer-active] .dsh-claude-stats-popover {",
1890
1911
  " position: fixed !important;",
1891
- " z-index: 100000 !important;",
1892
- " min-width: 260px !important;",
1912
+ " z-index: 99999 !important;",
1913
+ " min-width: 248px !important;",
1893
1914
  " max-width: min(380px, calc(100vw - 16px)) !important;",
1894
1915
  " box-sizing: border-box !important;",
1895
- " padding: 8px !important;",
1916
+ " padding: 6px !important;",
1896
1917
  " background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
1897
1918
  " border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
1898
1919
  " border-radius: 12px !important;",
1899
1920
  " box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
1900
1921
  " display: flex !important;",
1901
1922
  " flex-direction: column !important;",
1902
- " gap: 4px !important;",
1923
+ " gap: 6px !important;",
1903
1924
  " opacity: 0 !important;",
1904
1925
  " pointer-events: none !important;",
1905
1926
  " transform: translateY(4px) scale(0.98) !important;",
1927
+ " transform-origin: bottom left !important;",
1906
1928
  " transition: opacity 0.15s ease, transform 0.15s ease !important;",
1907
1929
  " font-family: var(--dsw-font-family) !important;",
1908
1930
  "}",
@@ -1940,8 +1962,9 @@ window.__ModuleLoader__.load({
1940
1962
  " display: flex !important;",
1941
1963
  " flex-direction: column !important;",
1942
1964
  " gap: 2px !important;",
1943
- " padding: 6px 8px !important;",
1944
- " border-radius: 8px !important;",
1965
+ " min-height: 32px !important;",
1966
+ " padding: 2px 7px !important;",
1967
+ " border-radius: 6px !important;",
1945
1968
  "}",
1946
1969
  "body[data-dsh-claude-style][data-dsh-claude-composer-active] .dsh-claude-stats-popover-item:hover {",
1947
1970
  " background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
@@ -2403,7 +2426,7 @@ window.__ModuleLoader__.load({
2403
2426
  "}",
2404
2427
  "body[data-dsh-claude-style] .dsh-claude-perm-popover {",
2405
2428
  " position: fixed !important;",
2406
- " min-width: 220px !important;",
2429
+ " min-width: 248px !important;",
2407
2430
  " background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
2408
2431
  " border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
2409
2432
  " border-radius: 12px !important;",
@@ -2527,10 +2550,10 @@ window.__ModuleLoader__.load({
2527
2550
  " box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
2528
2551
  " padding: 6px !important;",
2529
2552
  " box-sizing: border-box !important;",
2530
- " z-index: 1000 !important;",
2553
+ " z-index: 99999 !important;",
2531
2554
  " display: flex !important;",
2532
2555
  " flex-direction: column !important;",
2533
- " gap: 2px !important;",
2556
+ " gap: 6px !important;",
2534
2557
  " opacity: 0 !important;",
2535
2558
  " pointer-events: none !important;",
2536
2559
  " transform: translateY(6px) scale(0.98) !important;",
@@ -2563,8 +2586,9 @@ window.__ModuleLoader__.load({
2563
2586
  " strip is the inner row, so the hover plate covers the name and not the divider",
2564
2587
  " that follows it. */",
2565
2588
  "body[data-dsh-claude-style] .dsh-claude-account-popover-row {",
2566
- " padding: 6px 8px !important;",
2567
- " border-radius: 8px !important;",
2589
+ " min-height: 32px !important;",
2590
+ " padding: 2px 7px !important;",
2591
+ " border-radius: 6px !important;",
2568
2592
  " cursor: pointer !important;",
2569
2593
  " transition: background-color 0.12s ease !important;",
2570
2594
  "}",
@@ -3050,7 +3074,11 @@ window.__ModuleLoader__.load({
3050
3074
  " position: fixed !important;",
3051
3075
  " min-width: 248px !important;",
3052
3076
  " max-width: min(360px, calc(100vw - 16px)) !important;",
3053
- " max-height: min(440px, calc(100vh - 96px)) !important;",
3077
+ " /* The first level is sized by its content like the second: the cap is only",
3078
+ " the viewport, so a short list does not leave a tall empty card. The level-1",
3079
+ " list scrolls inside the body instead (see the scoped rule below); this",
3080
+ " overflow-y stays for the sub popover, whose whole card scrolls. */",
3081
+ " max-height: calc(100vh - 80px) !important;",
3054
3082
  " overflow-y: auto !important;",
3055
3083
  " background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
3056
3084
  " border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
@@ -3084,17 +3112,35 @@ window.__ModuleLoader__.load({
3084
3112
  " gap: 3px !important;",
3085
3113
  " min-width: 0 !important;",
3086
3114
  "}",
3115
+ "/* Level 1 splits into a scrollable list and a pinned footer: the model rows",
3116
+ " scroll under the divider / effort / more-models rows, which never leave the",
3117
+ " viewport edge of the card. Only the first level does this — the sub popover",
3118
+ " keeps scrolling as one card, with its provider labels pinned (below). */",
3119
+ "body[data-dsh-claude-style] .dsh-claude-model-popover:not(.dsh-claude-model-popover-sub) {",
3120
+ " overflow: hidden !important;",
3121
+ "}",
3122
+ "body[data-dsh-claude-style] .dsh-claude-model-popover:not(.dsh-claude-model-popover-sub) .dsh-claude-model-popover-body {",
3123
+ " flex: 1 1 auto !important;",
3124
+ " min-height: 0 !important;",
3125
+ " overflow-y: auto !important;",
3126
+ "}",
3127
+ "body[data-dsh-claude-style] .dsh-claude-model-footer {",
3128
+ " flex: none !important;",
3129
+ " display: flex !important;",
3130
+ " flex-direction: column !important;",
3131
+ " gap: 2px !important;",
3132
+ " min-width: 0 !important;",
3133
+ "}",
3087
3134
  "body[data-dsh-claude-style] .dsh-claude-model-status {",
3088
3135
  " color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
3089
3136
  " font-size: 12px !important;",
3090
3137
  " line-height: 20px !important;",
3091
3138
  " padding: 4px 8px !important;",
3092
3139
  "}",
3093
- "/* The sub popover is taller than the first level; its provider labels stay",
3094
- " pinned to the top while the list scrolls, the next provider replacing the",
3095
- " previous one. */",
3140
+ "/* The sub popover is taller than the first level and sized by its content: the",
3141
+ " cap is only the viewport, so a short provider list does not leave a tall empty",
3142
+ " card. Provider labels stay pinned to the top while the list scrolls. */",
3096
3143
  "body[data-dsh-claude-style] .dsh-claude-model-popover-sub {",
3097
- " max-height: min(540px, calc(100vh - 80px)) !important;",
3098
3144
  " padding-top: 0 !important;",
3099
3145
  "}",
3100
3146
  "",
@@ -3102,7 +3148,9 @@ window.__ModuleLoader__.load({
3102
3148
  " position: relative !important;",
3103
3149
  " display: flex !important;",
3104
3150
  " flex-direction: column !important;",
3105
- " gap: 3px !important;",
3151
+ " /* Level 2 rows sit close together: the 3px the body uses between sections",
3152
+ " read as a gap between models of the same provider. */",
3153
+ " gap: 1px !important;",
3106
3154
  " min-width: 0 !important;",
3107
3155
  "}",
3108
3156
  "",
@@ -3243,12 +3291,42 @@ window.__ModuleLoader__.load({
3243
3291
  " align-items: center !important;",
3244
3292
  " color: var(--dsw-alias-brand-primary, #d97757) !important;",
3245
3293
  "}",
3246
- "body[data-dsh-claude-style] .dsh-claude-model-divider {",
3294
+ "/* The picker's two hairlines share one declaration: the provider header's rule",
3295
+ " and the divider that closes the model list. Same colour, same thickness, same",
3296
+ " insets — the only difference left is that the header's line starts after the",
3297
+ " provider's name, which is the point of that row. */",
3298
+ "body[data-dsh-claude-style] .dsh-claude-model-divider,",
3299
+ "body[data-dsh-claude-style] .dsh-claude-model-rule::after {",
3247
3300
  " height: 1px !important;",
3248
3301
  " background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
3302
+ "}",
3303
+ "body[data-dsh-claude-style] .dsh-claude-model-divider {",
3304
+ " margin: 2px 4px !important;",
3305
+ " flex: none !important;",
3306
+ "}",
3307
+ "/* The provider header: the name first, then the rule running to the right edge.",
3308
+ " The line is what is left of that row, not a separate full-width rule above it",
3309
+ " — one rule per provider boundary, and nothing more. An empty label leaves the",
3310
+ " bare rule, which is how a provider that needs no naming (the official source)",
3311
+ " is separated. */",
3312
+ "body[data-dsh-claude-style] .dsh-claude-model-rule {",
3313
+ " display: flex !important;",
3314
+ " align-items: center !important;",
3315
+ " gap: 6px !important;",
3249
3316
  " margin: 2px 4px !important;",
3250
3317
  " flex: none !important;",
3251
3318
  "}",
3319
+ "body[data-dsh-claude-style] .dsh-claude-model-rule::after {",
3320
+ " content: \"\" !important;",
3321
+ " flex: 1 !important;",
3322
+ "}",
3323
+ "body[data-dsh-claude-style] .dsh-claude-model-rule-name {",
3324
+ " flex: none !important;",
3325
+ " font-size: 11px !important;",
3326
+ " line-height: 14px !important;",
3327
+ " color: var(--dsw-alias-label-caption, #a6a094) !important;",
3328
+ " letter-spacing: 0.02em !important;",
3329
+ "}",
3252
3330
  "/* Level-2 row: label + current value + chevron; hover opens its own level. */",
3253
3331
  "body[data-dsh-claude-style] .dsh-claude-model-cell {",
3254
3332
  " display: flex !important;",
@@ -3318,9 +3396,8 @@ window.__ModuleLoader__.load({
3318
3396
  "}",
3319
3397
  "body[data-dsh-claude-style] .dsh-claude-popover-item {",
3320
3398
  " width: 100% !important;",
3321
- " height: 32px !important;",
3322
3399
  " min-height: 32px !important;",
3323
- " padding: 0 8px !important;",
3400
+ " padding: 2px 7px !important;",
3324
3401
  " border-radius: 6px !important;",
3325
3402
  " border: none !important;",
3326
3403
  " background: transparent !important;",
@@ -3378,12 +3455,156 @@ window.__ModuleLoader__.load({
3378
3455
  " background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08)) !important;",
3379
3456
  " color: var(--dsw-alias-label-secondary, #787672) !important;",
3380
3457
  "}",
3458
+ "/* A provider the catalog no longer offers: its stored row stays checked but is",
3459
+ " muted, so it reads as \"this used to be picked\" and can be unchecked away. */",
3460
+ "body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-text,",
3461
+ "body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-badge {",
3462
+ " color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
3463
+ "}",
3464
+ "body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-badge {",
3465
+ " background: transparent !important;",
3466
+ "}",
3381
3467
  "",
3382
3468
  "/* Ensure popover items only display inside the popover */",
3383
3469
  "body[data-dsh-claude-style] [class*=\"footArea\"] > .dsh-claude-popover-item {",
3384
3470
  " display: none !important;",
3385
3471
  "}",
3386
3472
  "",
3473
+ "/* @composer-gate */",
3474
+ "/* ---------- Host menu primitive: the hero row's pickers ---------- */",
3475
+ "/* The workspace (directory) chip and the agent-preset seat open the host's",
3476
+ " shared Menu primitive, which portals its card to <body>. The card carries no",
3477
+ " marker of its own, so src/overrides/hero-menu.js stamps the one that belongs",
3478
+ " to this row with HERO_MENU_ATTR and these rules reach it alone — the host's",
3479
+ " other menus keep their own design. The metrics are the skin's popover",
3480
+ " language, the same card / rows / hover plate / accent check as",
3481
+ " components/model-picker.css and components/permissions.css. */",
3482
+ "",
3483
+ "/* Two class-name families meet here. The host's client-ui packages hash as",
3484
+ " `<hash>_<local>`, so their fragments read `_itemName` / `_itemDesc`; the",
3485
+ " shared menu ships inside the web shell and hashes as `_<local>_<hash>_<n>`,",
3486
+ " so its fragments read `_itemWrap_` / `_itemLabel_`. Both are the longest",
3487
+ " stable piece of the class — never the bare local name. */",
3488
+ "",
3489
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] {",
3490
+ " min-width: 248px !important;",
3491
+ " max-width: min(360px, calc(100vw - 16px)) !important;",
3492
+ " max-height: min(440px, calc(100vh - 96px)) !important;",
3493
+ " padding: 6px !important;",
3494
+ " gap: 6px !important;",
3495
+ " border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
3496
+ " border-radius: 12px !important;",
3497
+ " background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
3498
+ " box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
3499
+ " z-index: 99999 !important;",
3500
+ " font-family: var(--dsw-font-family) !important;",
3501
+ " /* The host mounts the card instead of toggling a `data-open` attribute, so",
3502
+ " the skin's open transition becomes a one-shot animation with the same",
3503
+ " values. Its origin is the edge nearest the trigger: the card sits beside it. */",
3504
+ " transform-origin: bottom left !important;",
3505
+ " animation: dsh-claude-hero-menu-in 0.15s ease !important;",
3506
+ "}",
3507
+ "@keyframes dsh-claude-hero-menu-in {",
3508
+ " from {",
3509
+ " opacity: 0;",
3510
+ " transform: translateY(4px) scale(0.98);",
3511
+ " }",
3512
+ " to {",
3513
+ " opacity: 1;",
3514
+ " transform: none;",
3515
+ " }",
3516
+ "}",
3517
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active][data-ds-dark-theme] [data-dsh-claude-hero-menu] {",
3518
+ " background: #1e1e1d !important;",
3519
+ " border-color: #2e2c29 !important;",
3520
+ " box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3) !important;",
3521
+ "}",
3522
+ "",
3523
+ "/* One row height, one radius, one hover plate for both pickers. */",
3524
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"] {",
3525
+ " min-height: 32px !important;",
3526
+ " padding: 2px 7px !important;",
3527
+ " gap: 8px !important;",
3528
+ " border-radius: 6px !important;",
3529
+ " font-size: 13px !important;",
3530
+ " line-height: 20px !important;",
3531
+ " color: var(--dsw-alias-label-primary, #141413) !important;",
3532
+ " transition: background-color 0.12s ease !important;",
3533
+ "}",
3534
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"]:hover:not(:disabled) {",
3535
+ " background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
3536
+ "}",
3537
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"]:disabled {",
3538
+ " opacity: 0.4 !important;",
3539
+ " cursor: not-allowed !important;",
3540
+ "}",
3541
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemIcon_\"] {",
3542
+ " width: 16px !important;",
3543
+ " height: 16px !important;",
3544
+ " color: var(--dsw-alias-label-secondary, #787672) !important;",
3545
+ "}",
3546
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemIcon_\"] svg {",
3547
+ " width: 16px !important;",
3548
+ " height: 16px !important;",
3549
+ "}",
3550
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemLabel_\"] {",
3551
+ " font-size: 13px !important;",
3552
+ " line-height: 20px !important;",
3553
+ "}",
3554
+ "/* The current row is marked by the accent, as in the model picker. */",
3555
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_check_\"] {",
3556
+ " flex: none !important;",
3557
+ " color: var(--dsw-alias-brand-primary, #d97757) !important;",
3558
+ "}",
3559
+ "",
3560
+ "/* The preset rows are two lines (name + description) and the workspace rows",
3561
+ " one with an icon, so the height above is a floor rather than a cap. */",
3562
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemName\"] {",
3563
+ " font-size: 13px !important;",
3564
+ " font-weight: 500 !important;",
3565
+ " line-height: 16px !important;",
3566
+ "}",
3567
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemDesc\"] {",
3568
+ " font-size: 11px !important;",
3569
+ " line-height: 14px !important;",
3570
+ " color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
3571
+ "}",
3572
+ "",
3573
+ "/* Heading and separator rows. */",
3574
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] > [class*=\"_viewport_\"] > [role=\"presentation\"] {",
3575
+ " padding: 6px 7px 2px !important;",
3576
+ " font-size: 11px !important;",
3577
+ " font-weight: 600 !important;",
3578
+ " line-height: 16px !important;",
3579
+ " letter-spacing: 0.04em !important;",
3580
+ " text-transform: uppercase !important;",
3581
+ " color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
3582
+ "}",
3583
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [role=\"separator\"] {",
3584
+ " height: 1px !important;",
3585
+ " margin: 2px 4px !important;",
3586
+ " background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
3587
+ "}",
3588
+ "",
3589
+ "/* The pinned footer (\"添加工作区…\") keeps the host's spacing with the skin's",
3590
+ " hairline. */",
3591
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] > [class*=\"_footer_\"] {",
3592
+ " margin-top: 4px !important;",
3593
+ " padding-top: 4px !important;",
3594
+ " border-top: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
3595
+ "}",
3596
+ "",
3597
+ "/* The card's padding is the skin's 6px rather than the host's 4px, so a side",
3598
+ " card moves with it: 6px pad + 6px gap. */",
3599
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_submenu_\"] {",
3600
+ " bottom: -6px !important;",
3601
+ " left: calc(100% + 12px) !important;",
3602
+ "}",
3603
+ "body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_submenu_\"]::before {",
3604
+ " left: -12px !important;",
3605
+ " width: 12px !important;",
3606
+ "}",
3607
+ "",
3387
3608
  "/* ---------- Footer takeover ----------",
3388
3609
  " Everything in this block modifies the HOST's own footer (it hides entries and",
3389
3610
  " zeroes the containers that host them) and exists only because the skin",
@@ -3715,6 +3936,32 @@ window.__ModuleLoader__.load({
3715
3936
  " color: var(--dsw-alias-label-caption);",
3716
3937
  "}",
3717
3938
  "",
3939
+ "/* The quick-provider trigger belongs to the same control family as the username",
3940
+ " input: one bordered plate that opens a picker. Its card is the shared popover",
3941
+ " card (components/permissions.css), so only the trigger is styled here. */",
3942
+ "body[data-dsh-claude-style] .dsh-claude-settings-picker {",
3943
+ " flex: none;",
3944
+ " min-width: 140px;",
3945
+ " max-width: 45%;",
3946
+ " box-sizing: border-box;",
3947
+ " border: 1px solid var(--dsw-alias-border-l1);",
3948
+ " border-radius: 8px;",
3949
+ " background: var(--dsw-alias-bg-layer-1);",
3950
+ " color: var(--dsw-alias-label-primary);",
3951
+ " padding: 6px 10px;",
3952
+ " font: inherit;",
3953
+ " font-size: 13px;",
3954
+ " line-height: 18px;",
3955
+ " text-align: left;",
3956
+ " cursor: pointer;",
3957
+ "}",
3958
+ "body[data-dsh-claude-style] .dsh-claude-settings-picker:hover {",
3959
+ " border-color: var(--dsw-alias-border-l2);",
3960
+ "}",
3961
+ "body[data-dsh-claude-style] .dsh-claude-settings-picker[aria-expanded=\"true\"] {",
3962
+ " border-color: var(--dsw-alias-brand-primary, #d97757);",
3963
+ "}",
3964
+ "",
3718
3965
  "/* ============================================================================",
3719
3966
  " 主题翻转:瞬时抑制过渡 (Theme Flip: Transition Suppression)",
3720
3967
  " ============================================================================ */",
@@ -3968,8 +4215,10 @@ window.__ModuleLoader__.load({
3968
4215
  var prefs = {
3969
4216
  brand: DEFAULT_BRAND,
3970
4217
  collapseFooter: true,
3971
- autoPopover: true,
4218
+ autoPopover: DEFAULT_AUTO_POPOVER,
3972
4219
  composerScope: 'all',
4220
+ modelPicker: true,
4221
+ quickProviders: [],
3973
4222
  username: '',
3974
4223
  banLocale: fallbackBanLocale || DEFAULT_BAN_LOCALE,
3975
4224
  }
@@ -4066,14 +4315,46 @@ window.__ModuleLoader__.load({
4066
4315
  } catch (error) { /* no fetch: defaults stay */ }
4067
4316
  }
4068
4317
 
4318
+ /**
4319
+ * Clamp the hover-open preference. It used to be a boolean, and a value
4320
+ * stored in that shape still has to land on a scope: `true` meant every
4321
+ * popover, `false` meant click-only.
4322
+ */
4323
+ function normalizeAutoPopover(value) {
4324
+ if (value === true) return AUTO_POPOVER_ALL
4325
+ if (value === false) return AUTO_POPOVER_OFF
4326
+ return AUTO_POPOVER_SCOPES.indexOf(value) === -1 ? DEFAULT_AUTO_POPOVER : value
4327
+ }
4328
+
4329
+ /**
4330
+ * The provider ids the picker's first level carries. Ids rather than names:
4331
+ * a provider can be renamed by the catalog at any time, and the stored
4332
+ * selection has to survive that. Order is the caller's, duplicates dropped.
4333
+ * The official service is the picker's default, not a choice, so a stored
4334
+ * id for it is dropped: the first level shows it whenever nothing else is
4335
+ * picked, which is what "default" means.
4336
+ */
4337
+ function normalizeQuickProviders(value) {
4338
+ if (!Array.isArray(value)) return []
4339
+ var out = []
4340
+ for (var i = 0; i < value.length; i++) {
4341
+ var id = value[i]
4342
+ if (typeof id !== 'string' || id === '' || id === MODEL_OFFICIAL_GROUP || out.indexOf(id) !== -1) continue
4343
+ out.push(id)
4344
+ }
4345
+ return out
4346
+ }
4347
+
4069
4348
  /** Clamp one host value into the preference shape (the host already did this). */
4070
4349
  function normalizePrefs(value) {
4071
4350
  var section = value && typeof value === 'object' ? value : {}
4072
4351
  return {
4073
4352
  brand: section.brand === BRAND_ANTHROPIC || section.brand === BRAND_OFF ? section.brand : BRAND_CLAUDE,
4074
4353
  collapseFooter: section.collapseFooter !== false,
4075
- autoPopover: section.autoPopover !== false,
4354
+ autoPopover: normalizeAutoPopover(section.autoPopover),
4076
4355
  composerScope: COMPOSER_SCOPES.indexOf(section.composerScope) === -1 ? 'all' : section.composerScope,
4356
+ modelPicker: section.modelPicker !== false,
4357
+ quickProviders: normalizeQuickProviders(section.quickProviders),
4077
4358
  username: (typeof section.username === 'string' ? section.username.trim().slice(0, USERNAME_MAX) : '') || fallbackUsername,
4078
4359
  banLocale: resolveBanLocale(section.banLocale),
4079
4360
  }
@@ -4151,19 +4432,6 @@ window.__ModuleLoader__.load({
4151
4432
  })
4152
4433
  }
4153
4434
 
4154
- /**
4155
- * The brand to show. `off` leaves the brand area entirely to the host, so
4156
- * the stylesheet matches neither brand variant for it.
4157
- *
4158
- * @param brand - the stored choice.
4159
- * @returns the brand actually applied.
4160
- */
4161
- function applyBrand(brand) {
4162
- var next = brand === BRAND_ANTHROPIC || brand === BRAND_OFF ? brand : BRAND_CLAUDE
4163
- document.body.setAttribute(BRAND_ATTR, next)
4164
- return next
4165
- }
4166
-
4167
4435
  /**
4168
4436
  * Model & settings localized copy.
4169
4437
  *
@@ -4326,9 +4594,13 @@ window.__ModuleLoader__.load({
4326
4594
  * the picker copy, so the page follows the shell language too — and the
4327
4595
  * English constants stay as the fallback for a failed fetch.
4328
4596
  */
4329
- function settingsCopy(key, fallback) {
4597
+ function settingsCopy(key, fallback, params) {
4330
4598
  var text = modelCopy === null || !modelCopy.settings ? '' : localized(modelCopy.settings[key])
4331
- return text || fallback
4599
+ if (!text) text = fallback
4600
+ if (!params) return text
4601
+ return text.replace(/\{(\w+)\}/g, function (match, name) {
4602
+ return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match
4603
+ })
4332
4604
  }
4333
4605
 
4334
4606
  /**
@@ -4730,10 +5002,10 @@ window.__ModuleLoader__.load({
4730
5002
  permHoverIntent = createHoverIntent(openPerm, closePermMenu, 150)
4731
5003
 
4732
5004
  btn.addEventListener('mouseenter', function () {
4733
- if (readPrefs().autoPopover) openPerm()
5005
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) openPerm()
4734
5006
  })
4735
5007
  btn.addEventListener('mouseleave', function () {
4736
- if (readPrefs().autoPopover) permHoverIntent.scheduleClose()
5008
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) permHoverIntent.scheduleClose()
4737
5009
  })
4738
5010
  popover.addEventListener('mouseenter', function () {
4739
5011
  permHoverIntent.cancel()
@@ -5240,16 +5512,18 @@ window.__ModuleLoader__.load({
5240
5512
  function bindStatsHover(root) {
5241
5513
  if (root.__dshStatsHoverBound) return
5242
5514
  root.__dshStatsHoverBound = true
5243
- // Gated like the account, model and permission popovers. The setting promises
5244
- // "hover opens these; off makes them click-to-open", and the stats popover was
5245
- // the one that opened on hover no matter what — so it ignored the switch. With
5246
- // the setting off, a click still reaches the host's own stats dialog, which is
5247
- // exactly what that button is for.
5515
+ // Gated like the account, model and permission popovers — and on the
5516
+ // strictest scope: the stats card is not a picker, so it only auto-opens
5517
+ // under "All". The setting promises "hover opens these; off makes them
5518
+ // click-to-open", and the stats popover was the one that opened on hover
5519
+ // no matter what — so it ignored the switch. With the setting off, a
5520
+ // click still reaches the host's own stats dialog, which is exactly what
5521
+ // that button is for.
5248
5522
  root.addEventListener('mouseenter', function () {
5249
- if (readPrefs().autoPopover) showStatsPopover(root)
5523
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) showStatsPopover(root)
5250
5524
  })
5251
5525
  root.addEventListener('mouseleave', function () {
5252
- if (readPrefs().autoPopover) scheduleHideStatsPopover()
5526
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) scheduleHideStatsPopover()
5253
5527
  })
5254
5528
  }
5255
5529
 
@@ -5296,7 +5570,8 @@ window.__ModuleLoader__.load({
5296
5570
  syncSegments()
5297
5571
  syncChatTabComposer()
5298
5572
  },
5299
- closeMenu: closePermMenu
5573
+ closeMenu: closePermMenu,
5574
+ closeStats: hideStatsPopover
5300
5575
  }
5301
5576
 
5302
5577
  return function () {
@@ -5326,15 +5601,6 @@ window.__ModuleLoader__.load({
5326
5601
  }
5327
5602
  }
5328
5603
 
5329
- // 从 model-picker.js 抽出(体量停止线:该文件已达 737/750 行)。碎片共享一个
5330
- // 工厂作用域,所以这里能直接看到 context/model-copy.js 的 modelCopy 与构建生成
5331
- // 的 COMBINE_SVGS / COMBINE_WORDS,
5332
- // 不需要 import;函数声明会提升,所以本文件在 build.mjs 的 FRAGMENTS 里的位置
5333
- // 不影响调用。
5334
- //
5335
- // modelEl 是行构建器共用的元素工厂,随它的消费者一起搬过来(原先与这些函数同
5336
- // 在 installModelPicker 内)。
5337
-
5338
5604
  function modelEl(tag, cls, text) {
5339
5605
  var el = document.createElement(tag)
5340
5606
  if (cls) el.className = cls
@@ -5353,11 +5619,10 @@ window.__ModuleLoader__.load({
5353
5619
  * model it did not make (an OpenCode row wearing OpenCode's mark beside
5354
5620
  * "LongCat-2.0"), which reads as a wrong answer rather than a missing one.
5355
5621
  *
5356
- * @param groupId - provider route id (unused; kept for call-site symmetry).
5357
5622
  * @param modelId - catalog model id.
5358
5623
  * @returns the vendored lockup's id, or null when no rule claims it.
5359
5624
  */
5360
- function modelBrand(groupId, modelId) {
5625
+ function modelBrand(modelId) {
5361
5626
  if (modelCopy !== null) {
5362
5627
  var id = String(modelId === void 0 || modelId === null ? '' : modelId).toLowerCase()
5363
5628
  for (var i = 0; i < modelCopy.brandRules.length; i++) {
@@ -5438,6 +5703,7 @@ window.__ModuleLoader__.load({
5438
5703
  var modelPop = null
5439
5704
  var modelSubPop = null
5440
5705
  var modelBody = null
5706
+ var modelFooter = null
5441
5707
  var modelSubBody = null
5442
5708
  var modelHoverIntent = createHoverIntent(openModelPopover, closeModelPopovers, 180)
5443
5709
  var modelDir = null
@@ -5447,6 +5713,8 @@ window.__ModuleLoader__.load({
5447
5713
  var modelSubKind = null
5448
5714
  var modelBodySig = ''
5449
5715
  var modelSubSig = ''
5716
+ /** Settings-page listeners waiting on the provider list. */
5717
+ var providerListeners = []
5450
5718
 
5451
5719
  /** Exact entry: `provider/model`, bare id, folded id, then the alias table. */
5452
5720
  function exactModelCopy(groupId, modelId) {
@@ -5597,7 +5865,7 @@ window.__ModuleLoader__.load({
5597
5865
  var store = modelDir.store
5598
5866
  if (store && typeof store.subscribe === 'function') {
5599
5867
  try {
5600
- modelSub = store.subscribe(function () { if (ui.schedule) ui.schedule() })
5868
+ modelSub = store.subscribe(function () { notifyProviders(); if (ui.schedule) ui.schedule() })
5601
5869
  } catch (error) {
5602
5870
  modelSub = null
5603
5871
  }
@@ -5611,6 +5879,37 @@ window.__ModuleLoader__.load({
5611
5879
  try { return modelDir.store.getSnapshot() } catch (error) { return null }
5612
5880
  }
5613
5881
 
5882
+ /**
5883
+ * The catalog's providers, in catalog order, each with its model count.
5884
+ *
5885
+ * The settings page's quick-provider picker is the other consumer, and it
5886
+ * may be opened before the picker itself ever was — so this resolves the
5887
+ * directory and starts the shared catalog load rather than requiring a
5888
+ * first popover open.
5889
+ */
5890
+ function modelProviders() {
5891
+ modelDirectory()
5892
+ warmModelCatalog()
5893
+ var snap = modelSnapshot()
5894
+ var groups = (snap && snap.groups) || []
5895
+ var out = []
5896
+ for (var i = 0; i < groups.length; i++) {
5897
+ if (groups[i].models.length === 0) continue
5898
+ out.push({ id: groups[i].id, name: groups[i].name || groups[i].id, count: groups[i].models.length })
5899
+ }
5900
+ return out
5901
+ }
5902
+
5903
+ /** Tell the settings picker the provider list moved (catalog arrived, changed). */
5904
+ function notifyProviders() {
5905
+ if (providerListeners.length === 0) return
5906
+ var list = modelProviders()
5907
+ var listeners = providerListeners.slice()
5908
+ for (var i = 0; i < listeners.length; i++) {
5909
+ try { listeners[i](list) } catch (error) { /* one listener must not block the rest */ }
5910
+ }
5911
+ }
5912
+
5614
5913
  /**
5615
5914
  * Start the host's model catalog load as soon as a session resolves, rather
5616
5915
  * than waiting for the first popover open.
@@ -5692,13 +5991,32 @@ window.__ModuleLoader__.load({
5692
5991
  var MODEL_CHECK_SVG = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.2 3.2L13 5"/></svg>'
5693
5992
  var MODEL_CHEVRON_SVG = '<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4l4 4-4 4"/></svg>'
5694
5993
 
5994
+ /** Catalog order is whatever the provider happened to send; id order is scannable. */
5995
+ function byModelId(a, b) {
5996
+ var left = String(a.id)
5997
+ var right = String(b.id)
5998
+ return left < right ? -1 : left > right ? 1 : 0
5999
+ }
6000
+
6001
+ /**
6002
+ * The rule that separates one provider's models from the next. The provider
6003
+ * name rides the rule itself rather than trailing the model in parentheses:
6004
+ * one quiet line above the group says who serves it, and the model names
6005
+ * stay clean.
6006
+ */
6007
+ function buildProviderRule(name) {
6008
+ var rule = modelEl('div', 'dsh-claude-model-rule')
6009
+ if (name) rule.appendChild(modelEl('span', 'dsh-claude-model-rule-name', name))
6010
+ return rule
6011
+ }
6012
+
5695
6013
  /** One selectable model row: brand mark, name, optional description line and a check when current. */
5696
6014
  function buildModelOption(group, model, selected, withDescription) {
5697
6015
  var item = modelEl('button', 'dsh-claude-model-option')
5698
6016
  item.type = 'button'
5699
6017
  item.setAttribute('role', 'menuitemradio')
5700
6018
  item.setAttribute('aria-checked', selected ? 'true' : 'false')
5701
- var brand = modelBrand(group.id, model.id)
6019
+ var brand = modelBrand(model.id)
5702
6020
  // The brand id is the row's styling hook — it is what gives a vendor's rows
5703
6021
  // their own typography (see .dsh-claude-model-name in
5704
6022
  // styles/components/model-picker.css). The vendor's mark is no longer drawn
@@ -5739,7 +6057,7 @@ window.__ModuleLoader__.load({
5739
6057
  cell.appendChild(chevron)
5740
6058
  cell.addEventListener('mouseenter', (function (k) {
5741
6059
  return function () {
5742
- if (readPrefs().autoPopover) openModelSub(k)
6060
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) openModelSub(k)
5743
6061
  }
5744
6062
  })(kind))
5745
6063
  cell.addEventListener('click', (function (k) {
@@ -5785,60 +6103,81 @@ window.__ModuleLoader__.load({
5785
6103
  var groups = (snap && snap.groups) || []
5786
6104
  var current = modelCurrent(snap)
5787
6105
  var effort = modelEffort(snap)
5788
- var sig = [status, activeLocale(), current ? current.group.id + '/' + current.model.id : '', effort ? String(effort.effective) : ''].join('|')
6106
+ var sig = [status, activeLocale(), current ? current.group.id + '/' + current.model.id : '', effort ? String(effort.effective) : '', readPrefs().quickProviders.join(',')].join('|')
5789
6107
  for (var g = 0; g < groups.length; g++) sig += ';' + groups[g].id + ':' + groups[g].models.length
5790
6108
  if (sig === modelBodySig) return
5791
6109
  modelBodySig = sig
5792
6110
  while (modelBody.firstChild) modelBody.removeChild(modelBody.firstChild)
6111
+ while (modelFooter && modelFooter.firstChild) modelFooter.removeChild(modelFooter.firstChild)
5793
6112
 
5794
6113
  if (status === 'idle' || status === 'loading' || status === 'selecting') {
5795
6114
  modelBody.appendChild(modelEl('div', 'dsh-claude-model-status', copyLabel('loading', MODEL_LOADING_LABEL)))
5796
6115
  } else {
5797
- var official = null
6116
+ // Level 1 always leads with the official service: it is the default
6117
+ // provider, not a choice in the settings, so it stays first no matter
6118
+ // what else is picked. The quick providers the settings page picked
6119
+ // follow it, each under its own rule. Only when the catalog has no
6120
+ // (non-empty) official service at all does the list fall back to the
6121
+ // picked providers, and with none picked to every provider.
6122
+ var sections = []
6123
+ var chosen = readPrefs().quickProviders
6124
+ for (var g0 = 0; g0 < groups.length; g0++) {
6125
+ if (groups[g0].id === MODEL_OFFICIAL_GROUP && groups[g0].models.length > 0) {
6126
+ sections.push(groups[g0])
6127
+ break
6128
+ }
6129
+ }
5798
6130
  for (var g2 = 0; g2 < groups.length; g2++) {
5799
- if (groups[g2].id === MODEL_OFFICIAL_GROUP) { official = groups[g2]; break }
6131
+ if (chosen.indexOf(groups[g2].id) === -1 || groups[g2].id === MODEL_OFFICIAL_GROUP || groups[g2].models.length === 0) continue
6132
+ sections.push(groups[g2])
5800
6133
  }
5801
- var rows = []
5802
- if (official) {
5803
- for (var m = 0; m < official.models.length; m++) rows.push({ group: official, model: official.models[m] })
5804
- } else {
5805
- for (var g3 = 0; g3 < groups.length; g3++) {
5806
- for (var m2 = 0; m2 < groups[g3].models.length; m2++) rows.push({ group: groups[g3], model: groups[g3].models[m2] })
6134
+ if (sections.length === 0) {
6135
+ for (var g4 = 0; g4 < groups.length; g4++) {
6136
+ if (groups[g4].models.length > 0) sections.push(groups[g4])
5807
6137
  }
5808
6138
  }
5809
- if (rows.length === 0) {
6139
+ if (sections.length === 0) {
5810
6140
  modelBody.appendChild(modelEl('div', 'dsh-claude-model-status', copyLabel('empty', MODEL_EMPTY_LABEL)))
5811
6141
  } else {
5812
- for (var r = 0; r < rows.length; r++) {
5813
- var selected = current !== null && current.group.id === rows[r].group.id && current.model.id === rows[r].model.id
5814
- modelBody.appendChild(buildModelOption(rows[r].group, rows[r].model, selected, true))
6142
+ for (var s = 0; s < sections.length; s++) {
6143
+ var section = sections[s]
6144
+ // The official source needs no naming, and the first section needs
6145
+ // no rule: a bare line above the list would be one line too many.
6146
+ var sectionLabel = section.id === MODEL_OFFICIAL_GROUP ? '' : (section.name || section.id)
6147
+ if (sectionLabel !== '' || s > 0) modelBody.appendChild(buildProviderRule(sectionLabel))
6148
+ var sectionModels = section.models.slice().sort(byModelId)
6149
+ for (var m = 0; m < sectionModels.length; m++) {
6150
+ var selected = current !== null && current.group.id === section.id && current.model.id === sectionModels[m].id
6151
+ modelBody.appendChild(buildModelOption(section, sectionModels[m], selected, true))
6152
+ }
5815
6153
  }
5816
6154
  }
5817
- modelBody.appendChild(modelEl('div', 'dsh-claude-model-divider'))
5818
- // When the active model is not on the official service, surface it under
5819
- // the official list so the current seat is still visible before the
5820
- // effort/More rows. It reads `model (provider)` in display names, never
5821
- // ids, and the provider trails the model instead of leading it: a
5822
- // wordmark may stand at the front of the label, and the seat has to say
5823
- // both which model it is on and which provider serves it — a wordmark
5824
- // names the vendor, not the route. It carries a description like any
5825
- // level-1 row, because this is the row the seat is read from.
5826
- if (current !== null && current.group.id !== MODEL_OFFICIAL_GROUP) {
6155
+ // The current seat is surfaced under the list when none of the sections
6156
+ // above already carries it, so the row the seat is read from is always
6157
+ // on screen. Its provider rides a rule of its own rather than trailing
6158
+ // the model in parentheses — the same idiom the sections use. It stays
6159
+ // above the divider: the divider closes the model list, so anything
6160
+ // that belongs to the list has to sit on its side of it.
6161
+ var currentListed = false
6162
+ for (var c = 0; c < sections.length; c++) {
6163
+ if (current !== null && sections[c].id === current.group.id) currentListed = true
6164
+ }
6165
+ if (current !== null && !currentListed) {
6166
+ // The divider above already draws a line, so a provider that needs no
6167
+ // naming (the official source) adds nothing here.
6168
+ var currentRuleName = current.group.id === MODEL_OFFICIAL_GROUP ? '' : (current.group.name || current.group.id)
6169
+ if (currentRuleName !== '') modelBody.appendChild(buildProviderRule(currentRuleName))
5827
6170
  var currentRow = modelEl('button', 'dsh-claude-model-option')
5828
6171
  currentRow.type = 'button'
5829
6172
  currentRow.setAttribute('role', 'menuitemradio')
5830
6173
  currentRow.setAttribute('aria-checked', 'true')
5831
- var currentBrand = modelBrand(current.group.id, current.model.id)
6174
+ var currentBrand = modelBrand(current.model.id)
5832
6175
  var currentName = current.model.name || current.model.id
5833
6176
  // The brand id is the row's styling hook here too, so this row wears
5834
6177
  // the same vendor lockup and face as the list entry it stands for.
5835
6178
  if (currentBrand) currentRow.setAttribute('data-brand', currentBrand)
5836
6179
  var currentCopy = modelEl('span', 'dsh-claude-model-copy')
5837
6180
  var currentLabel = buildModelLabel(currentName, currentBrand)
5838
- // The provider's own display name; the route id only stands in when
5839
- // the catalog gives the group no name.
5840
- var currentProvider = current.group.name || current.group.id
5841
- if (currentProvider) currentLabel.appendChild(document.createTextNode(' (' + currentProvider + ')'))
5842
6181
  currentCopy.appendChild(currentLabel)
5843
6182
  var currentDesc = modelDescription(current.group.id, current.model)
5844
6183
  if (currentDesc) currentCopy.appendChild(modelEl('span', 'dsh-claude-model-desc', currentDesc))
@@ -5852,8 +6191,14 @@ window.__ModuleLoader__.load({
5852
6191
  })
5853
6192
  modelBody.appendChild(currentRow)
5854
6193
  }
5855
- if (effort) modelBody.appendChild(buildModelCell(copyLabel('effortLabel', MODEL_EFFORT_LABEL), effort.label, 'effort'))
5856
- modelBody.appendChild(buildModelCell(copyLabel('moreLabel', MODEL_MORE_LABEL), '', 'more'))
6194
+ // The divider closes the model list and the two drill rows follow it;
6195
+ // all three live in the footer, OUTSIDE the scroll area — the list
6196
+ // above scrolls under them while the controls stay reachable.
6197
+ if (modelFooter) {
6198
+ modelFooter.appendChild(modelEl('div', 'dsh-claude-model-divider'))
6199
+ if (effort) modelFooter.appendChild(buildModelCell(copyLabel('effortLabel', MODEL_EFFORT_LABEL), effort.label, 'effort'))
6200
+ modelFooter.appendChild(buildModelCell(copyLabel('moreLabel', MODEL_MORE_LABEL), '', 'more'))
6201
+ }
5857
6202
  }
5858
6203
  }
5859
6204
 
@@ -5918,11 +6263,7 @@ window.__ModuleLoader__.load({
5918
6263
  // A provider's models read in id order, so the list is scannable and stays
5919
6264
  // put between visits; the catalog's own order is whatever the provider
5920
6265
  // happened to send. Sorted on a copy — the snapshot belongs to the store.
5921
- var groupModels = group.models.slice().sort(function (a, b) {
5922
- var left = String(a.id)
5923
- var right = String(b.id)
5924
- return left < right ? -1 : left > right ? 1 : 0
5925
- })
6266
+ var groupModels = group.models.slice().sort(byModelId)
5926
6267
  for (var m = 0; m < groupModels.length; m++) {
5927
6268
  var selected = current !== null && current.group.id === group.id && current.model.id === groupModels[m].id
5928
6269
  groupSection.appendChild(buildModelOption(group, groupModels[m], selected, false))
@@ -5958,6 +6299,9 @@ window.__ModuleLoader__.load({
5958
6299
  modelBody = document.createElement('div')
5959
6300
  modelBody.className = 'dsh-claude-model-popover-body'
5960
6301
  modelPop.appendChild(modelBody)
6302
+ modelFooter = document.createElement('div')
6303
+ modelFooter.className = 'dsh-claude-model-footer'
6304
+ modelPop.appendChild(modelFooter)
5961
6305
  modelPop.addEventListener('mouseenter', cancelCloseModel)
5962
6306
  modelPop.addEventListener('mouseleave', scheduleCloseModel)
5963
6307
  document.body.appendChild(modelPop)
@@ -5978,7 +6322,10 @@ window.__ModuleLoader__.load({
5978
6322
 
5979
6323
  /** Build/refresh the trigger, its label and the popover rows. */
5980
6324
  function syncModelControl() {
5981
- if (!ui.copy.isComposerActive()) {
6325
+ // Two ways to be off: the composer restyle does not apply to this page, or
6326
+ // the picker preference is off. Both hand the host's own model seat and
6327
+ // menu back, so both run the same sweep.
6328
+ if (!ui.copy.isComposerActive() || !readPrefs().modelPicker) {
5982
6329
  var allHosts = document.querySelectorAll('[data-dsh-claude-model-host]')
5983
6330
  for (var h = 0; h < allHosts.length; h++) {
5984
6331
  allHosts[h].removeAttribute('data-dsh-claude-model-host')
@@ -5995,6 +6342,7 @@ window.__ModuleLoader__.load({
5995
6342
  modelPop = null
5996
6343
  modelSubPop = null
5997
6344
  modelBody = null
6345
+ modelFooter = null
5998
6346
  modelSubBody = null
5999
6347
  modelSubKind = null
6000
6348
  modelBodySig = ''
@@ -6014,6 +6362,18 @@ window.__ModuleLoader__.load({
6014
6362
  var slot = document.querySelector('[data-slot="conversation.input.model"]')
6015
6363
  if (slot === null) return
6016
6364
  // Hide the host's own seat (React owns the node; re-mark on swap).
6365
+ // Idempotent against a torn-down-less reload, like the account footer:
6366
+ // client HMR drops the old fiber's disposals, so a previous generation's
6367
+ // trigger and popovers are still in the DOM while this fresh scope starts
6368
+ // from null. Sweep the strays, or the seat renders twice.
6369
+ var strayBtns = slot.querySelectorAll('.dsh-claude-model-btn')
6370
+ for (var sb = 0; sb < strayBtns.length; sb++) {
6371
+ if (strayBtns[sb] !== modelBtn) strayBtns[sb].parentElement.removeChild(strayBtns[sb])
6372
+ }
6373
+ var strayPops = document.querySelectorAll('body > .dsh-claude-model-popover')
6374
+ for (var sp = 0; sp < strayPops.length; sp++) {
6375
+ if (strayPops[sp] !== modelPop && strayPops[sp] !== modelSubPop) strayPops[sp].parentElement.removeChild(strayPops[sp])
6376
+ }
6017
6377
  var hostRoot = slot.firstElementChild
6018
6378
  if (hostRoot !== null && !hostRoot.hasAttribute('data-dsh-claude-model-host')) {
6019
6379
  hostRoot.setAttribute('data-dsh-claude-model-host', '')
@@ -6025,13 +6385,13 @@ window.__ModuleLoader__.load({
6025
6385
  modelBtn.className = 'dsh-claude-model-btn'
6026
6386
  modelBtn.setAttribute('aria-haspopup', 'menu')
6027
6387
  modelBtn.innerHTML = '<span class="dsh-claude-model-btn-label"></span>'
6028
- // Same contract as the account trigger: hover unless the preference
6029
- // says click-only.
6388
+ // Same contract as the account trigger: hover under the "All" scope,
6389
+ // click-only otherwise.
6030
6390
  modelBtn.addEventListener('mouseenter', function () {
6031
- if (readPrefs().autoPopover) openModelPopover()
6391
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) openModelPopover()
6032
6392
  })
6033
6393
  modelBtn.addEventListener('mouseleave', function () {
6034
- if (readPrefs().autoPopover) scheduleCloseModel()
6394
+ if (readPrefs().autoPopover === AUTO_POPOVER_ALL) scheduleCloseModel()
6035
6395
  })
6036
6396
  modelBtn.addEventListener('click', function (e) {
6037
6397
  e.stopPropagation()
@@ -6083,6 +6443,14 @@ window.__ModuleLoader__.load({
6083
6443
  (modelSubPop !== null && modelSubPop.contains(target))
6084
6444
  },
6085
6445
  reposition: positionModelPopovers,
6446
+ providers: modelProviders,
6447
+ onProviders: function (listener) {
6448
+ providerListeners.push(listener)
6449
+ return function () {
6450
+ var at = providerListeners.indexOf(listener)
6451
+ if (at !== -1) providerListeners.splice(at, 1)
6452
+ }
6453
+ },
6086
6454
  invalidateCopy: function () {
6087
6455
  modelBodySig = ''
6088
6456
  modelSubSig = ''
@@ -6097,6 +6465,7 @@ window.__ModuleLoader__.load({
6097
6465
  modelPop = null
6098
6466
  modelSubPop = null
6099
6467
  modelBody = null
6468
+ modelFooter = null
6100
6469
  modelSubBody = null
6101
6470
  modelSubKind = null
6102
6471
  modelBodySig = ''
@@ -6107,6 +6476,331 @@ window.__ModuleLoader__.load({
6107
6476
  return ui.model.teardown
6108
6477
  }
6109
6478
 
6479
+ /**
6480
+ * The hero row's two pickers — the workspace (directory) chip and the
6481
+ * agent-preset seat — both open the host's shared menu primitive, which
6482
+ * portals its card to <body>. That card carries no marker of its own, so a
6483
+ * stylesheet cannot tell it apart from the host's other menus (sidebar row
6484
+ * menus, the settings permission row, submenus) — and it has to: this skin
6485
+ * redraws these two and leaves the rest alone.
6486
+ *
6487
+ * So the browser half stamps the open card rather than guessing in CSS.
6488
+ * While either trigger reports `aria-expanded="true"`, the one host menu
6489
+ * sitting in <body> is that card; HERO_MENU_ATTR is what
6490
+ * components/hero-menu.css switches on.
6491
+ *
6492
+ * The scheduler's attributeFilter does not watch `aria-expanded`, but the
6493
+ * card is inserted into <body> as it opens — a childList mutation the pass
6494
+ * already observes — so the stamp lands on the frame the menu appears. Two
6495
+ * candidates mean an unrelated menu is open as well: the pass then leaves
6496
+ * every card alone rather than stamping the wrong one.
6497
+ *
6498
+ * The host drops that card below its trigger, which is exactly where the
6499
+ * composer sits: it would land on the input area and on the controls in it.
6500
+ * This row is *above* the composer, so the card belongs beside the trigger
6501
+ * instead — bottom-aligned with it and growing upward into the empty hero
6502
+ * space. The host re-places the card on every scroll and resize, so this
6503
+ * position is written with `important` (which beats its inline write) and
6504
+ * re-applied from the scheduler's scroll/resize hook.
6505
+ *
6506
+ * Hover is the host's own click handler driven from here: the "Open popovers
6507
+ * on hover" preference's `all` scope covers these two, and the host offers no
6508
+ * hover of its own. The listeners are delegated from the document because
6509
+ * React replaces the triggers, and the close waits out the gap between the
6510
+ * trigger and the card so crossing it does not shut the menu.
6511
+ */
6512
+ function installHeroMenu(ctx, ui) {
6513
+ /** Air between the trigger and the card, and the viewport's own margin. */
6514
+ var GAP = 6
6515
+ var MARGIN = 12
6516
+ /** Long enough to cross the gap above, short enough to still read as hover. */
6517
+ var CLOSE_DELAY = 160
6518
+ /** The two triggers, and the card once it is stamped. */
6519
+ var TRIGGER_SELECTOR = '[class*="heroWorkspaceRow"] [aria-haspopup="menu"]'
6520
+ var CARD_SELECTOR = '[' + HERO_MENU_ATTR + ']'
6521
+ var stamped = null
6522
+ var stampedTrigger = null
6523
+ var closeTimer = null
6524
+ var openedByHover = false
6525
+
6526
+ function cancelHoverClose() {
6527
+ if (closeTimer === null) return
6528
+ clearTimeout(closeTimer)
6529
+ closeTimer = null
6530
+ }
6531
+
6532
+ /** Close what hover opened; a click-opened menu is left alone. */
6533
+ function closeFromHover() {
6534
+ closeTimer = null
6535
+ if (!openedByHover) return
6536
+ var trigger = openTrigger()
6537
+ if (trigger !== null) trigger.click()
6538
+ openedByHover = false
6539
+ }
6540
+
6541
+ function scheduleHoverClose() {
6542
+ cancelHoverClose()
6543
+ closeTimer = setTimeout(closeFromHover, CLOSE_DELAY)
6544
+ }
6545
+
6546
+ /** The `all` scope only, and only while the composer restyle is in play. */
6547
+ function hoverEnabled() {
6548
+ return readPrefs().autoPopover === AUTO_POPOVER_ALL &&
6549
+ ui.copy !== undefined && ui.copy.isComposerActive()
6550
+ }
6551
+
6552
+ function closestWithin(target, selector) {
6553
+ if (target === null || target === undefined || typeof target.closest !== 'function') return null
6554
+ return target.closest(selector)
6555
+ }
6556
+
6557
+ function onHeroPointerOver(e) {
6558
+ if (!hoverEnabled()) return
6559
+ var target = e.target
6560
+ if (closestWithin(target, CARD_SELECTOR) !== null) {
6561
+ cancelHoverClose()
6562
+ return
6563
+ }
6564
+ var trigger = closestWithin(target, TRIGGER_SELECTOR)
6565
+ if (trigger === null) return
6566
+ cancelHoverClose()
6567
+ if (trigger.getAttribute('aria-expanded') !== 'true') {
6568
+ trigger.click()
6569
+ openedByHover = true
6570
+ }
6571
+ }
6572
+
6573
+ function onHeroPointerOut(e) {
6574
+ if (!hoverEnabled()) return
6575
+ var target = e.target
6576
+ if (closestWithin(target, CARD_SELECTOR) === null && closestWithin(target, TRIGGER_SELECTOR) === null) return
6577
+ // Moving onto the other half — the card, or the trigger — is not a leave.
6578
+ var next = e.relatedTarget
6579
+ if (closestWithin(next, CARD_SELECTOR) !== null || closestWithin(next, TRIGGER_SELECTOR) !== null) return
6580
+ scheduleHoverClose()
6581
+ }
6582
+
6583
+ document.addEventListener('mouseover', onHeroPointerOver, true)
6584
+ document.addEventListener('mouseout', onHeroPointerOut, true)
6585
+
6586
+ function clearStamp() {
6587
+ if (stamped === null) return
6588
+ stamped.removeAttribute(HERO_MENU_ATTR)
6589
+ stamped = null
6590
+ stampedTrigger = null
6591
+ }
6592
+
6593
+ /** The trigger whose picker is open, or null. */
6594
+ function openTrigger() {
6595
+ return document.querySelector('[class*="heroWorkspaceRow"] [aria-haspopup="menu"][aria-expanded="true"]') ||
6596
+ document.querySelector('[class*="cardWorkspaceTrigger"] [aria-expanded="true"]')
6597
+ }
6598
+
6599
+ /**
6600
+ * Park the card beside its trigger: to its right, bottom-aligned so it
6601
+ * grows upward, flipped to the left when the viewport is tight, and clamped
6602
+ * to the viewport either way. A card that has not been laid out yet is left
6603
+ * for the next pass rather than pinned to a zero-sized guess.
6604
+ */
6605
+ function placeCard(trigger, card) {
6606
+ var rect = trigger.getBoundingClientRect()
6607
+ var width = card.offsetWidth
6608
+ var height = card.offsetHeight
6609
+ if (width === 0 || height === 0) return
6610
+ var left = rect.right + GAP
6611
+ if (left + width > window.innerWidth - MARGIN) left = rect.left - GAP - width
6612
+ left = Math.max(MARGIN, Math.min(left, window.innerWidth - width - MARGIN))
6613
+ var top = rect.bottom - height
6614
+ top = Math.max(MARGIN, Math.min(top, window.innerHeight - height - MARGIN))
6615
+ card.style.setProperty('left', Math.round(left) + 'px', 'important')
6616
+ card.style.setProperty('top', Math.round(top) + 'px', 'important')
6617
+ }
6618
+
6619
+ /** Re-place an open card after a scroll or a resize moved its anchor. */
6620
+ function repositionHeroMenu() {
6621
+ if (stamped === null || stampedTrigger === null) return
6622
+ placeCard(stampedTrigger, stamped)
6623
+ }
6624
+
6625
+ function syncHeroMenu() {
6626
+ var trigger = openTrigger()
6627
+ if (trigger === null) {
6628
+ // The menu is shut — by a click, by Escape or by an outside press — so
6629
+ // whatever hover opened it no longer owns it.
6630
+ openedByHover = false
6631
+ cancelHoverClose()
6632
+ clearStamp()
6633
+ return
6634
+ }
6635
+ // The skin's own popovers carry their own classes, so excluding them
6636
+ // leaves the host's menus only.
6637
+ var cards = document.querySelectorAll('body > [role="menu"]:not([class*="dsh-claude"])')
6638
+ if (cards.length !== 1) {
6639
+ clearStamp()
6640
+ return
6641
+ }
6642
+ if (stamped !== cards[0]) {
6643
+ clearStamp()
6644
+ stamped = cards[0]
6645
+ stampedTrigger = trigger
6646
+ stamped.setAttribute(HERO_MENU_ATTR, '')
6647
+ }
6648
+ placeCard(trigger, stamped)
6649
+ }
6650
+
6651
+ ui.heroMenu = { sync: syncHeroMenu, reposition: repositionHeroMenu }
6652
+ return function () {
6653
+ cancelHoverClose()
6654
+ openedByHover = false
6655
+ clearStamp()
6656
+ document.removeEventListener('mouseover', onHeroPointerOver, true)
6657
+ document.removeEventListener('mouseout', onHeroPointerOut, true)
6658
+ delete ui.heroMenu
6659
+ }
6660
+ }
6661
+
6662
+ /**
6663
+ * The settings page's quick-provider picker — the providers whose models the
6664
+ * model picker's first level carries.
6665
+ *
6666
+ * It is the permission popover's design in multi-select form: the same card
6667
+ * and the same rows (`.dsh-claude-perm-popover` is that shared card, the name
6668
+ * is historical), with the accent check marking the chosen providers. It is a
6669
+ * settings control rather than a takeover, so it opens on click only —
6670
+ * hovering a settings row should not unfold a menu — and it stays open while
6671
+ * rows are toggled.
6672
+ *
6673
+ * The card is appended to <body> rather than rendered inside the settings row:
6674
+ * a dialog clips and may transform its own subtree, so a `position: fixed`
6675
+ * card in there would be positioned against the dialog, not the viewport.
6676
+ */
6677
+ function installQuickProviders(ctx, ui) {
6678
+ var CHECK_SVG = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.2 3.2L13 5"/></svg>'
6679
+ var card = null
6680
+ var cardBody = null
6681
+ var anchor = null
6682
+ var onWrite = null
6683
+ var providers = []
6684
+ var unsubscribe = null
6685
+
6686
+ function close() {
6687
+ if (card !== null && card.parentElement) card.parentElement.removeChild(card)
6688
+ if (anchor !== null) anchor.setAttribute('aria-expanded', 'false')
6689
+ card = null
6690
+ cardBody = null
6691
+ anchor = null
6692
+ onWrite = null
6693
+ document.removeEventListener('pointerdown', onOutsidePointerDown, true)
6694
+ document.removeEventListener('keydown', onKeyDown, true)
6695
+ }
6696
+
6697
+ function onOutsidePointerDown(e) {
6698
+ if (card === null) return
6699
+ var target = e.target
6700
+ if (card.contains(target)) return
6701
+ if (anchor !== null && anchor.contains(target)) return
6702
+ close()
6703
+ }
6704
+
6705
+ function onKeyDown(e) {
6706
+ if (e.key === 'Escape') close()
6707
+ }
6708
+
6709
+ /** One provider: its name, how many models it offers, and a check when chosen. */
6710
+ function buildRow(provider, chosen, stale) {
6711
+ var row = modelEl('button', 'dsh-claude-popover-item')
6712
+ if (stale) row.classList.add('dsh-claude-model-stale')
6713
+ row.type = 'button'
6714
+ row.setAttribute('role', 'menuitemcheckbox')
6715
+ row.setAttribute('aria-checked', chosen ? 'true' : 'false')
6716
+ row.appendChild(modelEl('span', 'dsh-claude-popover-item-text', provider.name))
6717
+ row.appendChild(modelEl('span', 'dsh-claude-popover-item-badge', stale ? settingsCopy('quickRemoved', 'Removed') : String(provider.count)))
6718
+ var check = modelEl('span', 'dsh-claude-model-check')
6719
+ check.innerHTML = chosen ? CHECK_SVG : ''
6720
+ row.appendChild(check)
6721
+ row.addEventListener('click', function (e) {
6722
+ e.stopPropagation()
6723
+ var next = readPrefs().quickProviders.slice()
6724
+ var at = next.indexOf(provider.id)
6725
+ if (at === -1) next.push(provider.id)
6726
+ else next.splice(at, 1)
6727
+ if (typeof onWrite === 'function') onWrite(next)
6728
+ renderBody()
6729
+ })
6730
+ return row
6731
+ }
6732
+
6733
+ function renderBody() {
6734
+ if (cardBody === null) return
6735
+ while (cardBody.firstChild) cardBody.removeChild(cardBody.firstChild)
6736
+ if (providers.length === 0) {
6737
+ cardBody.appendChild(modelEl('div', 'dsh-claude-model-status', settingsCopy('quickLoading', 'Loading providers…')))
6738
+ return
6739
+ }
6740
+ var chosen = readPrefs().quickProviders
6741
+ // The official service always leads the first level, so it is not a
6742
+ // choice here: it is never listed, and a stored id for it is pruned on
6743
+ // load. A provider that vanished from the catalog still appears, marked
6744
+ // removed, so it can be unchecked out of the stored list instead of
6745
+ // lingering invisibly.
6746
+ var known = {}
6747
+ for (var i = 0; i < providers.length; i++) {
6748
+ if (providers[i].id === MODEL_OFFICIAL_GROUP) continue
6749
+ known[providers[i].id] = true
6750
+ cardBody.appendChild(buildRow(providers[i], chosen.indexOf(providers[i].id) !== -1, false))
6751
+ }
6752
+ for (var s = 0; s < chosen.length; s++) {
6753
+ if (chosen[s] === MODEL_OFFICIAL_GROUP || known[chosen[s]]) continue
6754
+ cardBody.appendChild(buildRow({ id: chosen[s], name: chosen[s], count: 0 }, true, true))
6755
+ }
6756
+ }
6757
+
6758
+ function open(trigger, write) {
6759
+ anchor = trigger
6760
+ anchor.setAttribute('aria-expanded', 'true')
6761
+ onWrite = write
6762
+ providers = ui.model && typeof ui.model.providers === 'function' ? ui.model.providers() : []
6763
+ card = modelEl('div', 'dsh-claude-perm-popover')
6764
+ card.setAttribute('role', 'menu')
6765
+ card.setAttribute('data-open', 'true')
6766
+ cardBody = modelEl('div', 'dsh-claude-model-popover-body')
6767
+ card.appendChild(cardBody)
6768
+ renderBody()
6769
+ document.body.appendChild(card)
6770
+ positionAnchoredPopover(trigger, card, { side: 'above', gap: 6, important: true })
6771
+ document.addEventListener('pointerdown', onOutsidePointerDown, true)
6772
+ document.addEventListener('keydown', onKeyDown, true)
6773
+ }
6774
+
6775
+ function toggle(trigger, write) {
6776
+ if (card === null) open(trigger, write)
6777
+ else close()
6778
+ }
6779
+
6780
+ // The catalog arrives after the settings page may already be open, so an
6781
+ // open card follows it rather than showing an empty list until reopened.
6782
+ if (ui.model && typeof ui.model.onProviders === 'function') {
6783
+ unsubscribe = ui.model.onProviders(function (list) {
6784
+ providers = list
6785
+ if (card !== null) renderBody()
6786
+ })
6787
+ }
6788
+
6789
+ ui.quickProviders = {
6790
+ toggle: toggle,
6791
+ close: close,
6792
+ isOpen: function () { return card !== null },
6793
+ }
6794
+ return function () {
6795
+ close()
6796
+ if (unsubscribe !== null) {
6797
+ try { unsubscribe() } catch (error) { /* already disposed */ }
6798
+ unsubscribe = null
6799
+ }
6800
+ delete ui.quickProviders
6801
+ }
6802
+ }
6803
+
6110
6804
  function installAccountFooter(ctx, ui) {
6111
6805
  var accountBtn = null
6112
6806
  var accountPopover = null
@@ -6696,6 +7390,20 @@ window.__ModuleLoader__.load({
6696
7390
  }
6697
7391
  var username = getUsername(ctx)
6698
7392
 
7393
+ // Idempotent against a torn-down-less reload: client HMR drops the old
7394
+ // fiber's disposals instead of running them, so a previous generation's
7395
+ // button and popover are still in the DOM and this fresh scope knows
7396
+ // nothing of them. Sweep the strays before deciding whether to build —
7397
+ // otherwise the row renders twice (and the orphan keeps a stale name).
7398
+ var strayBtns = footArea.querySelectorAll('.dsh-claude-account-btn')
7399
+ for (var sb = 0; sb < strayBtns.length; sb++) {
7400
+ if (strayBtns[sb] !== accountBtn) strayBtns[sb].parentElement.removeChild(strayBtns[sb])
7401
+ }
7402
+ var strayPops = document.querySelectorAll('body > .dsh-claude-account-popover')
7403
+ for (var sp = 0; sp < strayPops.length; sp++) {
7404
+ if (strayPops[sp] !== accountPopover) strayPops[sp].parentElement.removeChild(strayPops[sp])
7405
+ }
7406
+
6699
7407
  if (accountBtn === null || !footArea.contains(accountBtn)) {
6700
7408
  if (accountBtn && accountBtn.parentElement) accountBtn.parentElement.removeChild(accountBtn)
6701
7409
  accountBtn = document.createElement('div')
@@ -6714,12 +7422,13 @@ window.__ModuleLoader__.load({
6714
7422
  // Hover is the default way in; the "Open popovers on hover"
6715
7423
  // preference turns it off, leaving the click handler below as the only
6716
7424
  // way in (and the only way out, so a click-opened popover does not
6717
- // vanish when the pointer leaves).
7425
+ // vanish when the pointer leaves). The account row is the one popover
7426
+ // the `account` scope keeps on hover.
6718
7427
  accountBtn.addEventListener('mouseenter', function () {
6719
- if (readPrefs().autoPopover) openPopover()
7428
+ if (readPrefs().autoPopover !== AUTO_POPOVER_OFF) openPopover()
6720
7429
  })
6721
7430
  accountBtn.addEventListener('mouseleave', function () {
6722
- if (readPrefs().autoPopover && !popoverOpenedByClick) scheduleClosePopover()
7431
+ if (readPrefs().autoPopover !== AUTO_POPOVER_OFF && !popoverOpenedByClick) scheduleClosePopover()
6723
7432
  })
6724
7433
  accountBtn.addEventListener('click', function (e) {
6725
7434
  e.stopPropagation()
@@ -7339,6 +8048,23 @@ window.__ModuleLoader__.load({
7339
8048
  }
7340
8049
  }
7341
8050
 
8051
+ // Focus moving into the composer means the user is about to type: every
8052
+ // popover the skin keeps open around the card is in the way there, so all
8053
+ // of them close. `focusin` bubbles (unlike focus), so one listener covers
8054
+ // the card and everything inside it; the observer's attributeFilter does
8055
+ // not watch focus events, so this cannot feed itself another pass.
8056
+ function onComposerFocusIn(e) {
8057
+ var target = e.target
8058
+ if (!target || typeof target.closest !== 'function') return
8059
+ if (target.closest('[data-composer-card]') === null) return
8060
+ if (ui.model) ui.model.close()
8061
+ if (ui.permissions) ui.permissions.closeMenu()
8062
+ if (ui.permissions && ui.permissions.closeStats) ui.permissions.closeStats()
8063
+ if (ui.footer) ui.footer.close()
8064
+ if (ui.heroMenu) ui.heroMenu.close()
8065
+ if (ui.quickProviders) ui.quickProviders.close()
8066
+ }
8067
+
7342
8068
  function onComposerInput(e) {
7343
8069
  var target = e.target
7344
8070
  if (!target) return
@@ -7352,12 +8078,14 @@ window.__ModuleLoader__.load({
7352
8078
  document.addEventListener('keydown', onGlobalKeyDown, true)
7353
8079
  document.addEventListener('input', onComposerInput, true)
7354
8080
  document.addEventListener('compositionend', onComposerInput, true)
8081
+ document.addEventListener('focusin', onComposerFocusIn, true)
7355
8082
 
7356
8083
  // Both fixed popovers are anchored to their trigger; scroll of the page
7357
8084
  // (not the conversation's own auto-stick) and resizes move the anchor, so
7358
8085
  // whichever is open must re-resolve it.
7359
8086
  function onFixedPopoverViewportChange() {
7360
8087
  if (ui.model) ui.model.reposition()
8088
+ if (ui.heroMenu) ui.heroMenu.reposition()
7361
8089
  if (ui.footer && ui.footer.isOpen()) ui.footer.reposition()
7362
8090
  }
7363
8091
  window.addEventListener('resize', onFixedPopoverViewportChange)
@@ -7429,6 +8157,7 @@ window.__ModuleLoader__.load({
7429
8157
  if (ui.copy) ui.copy.sync()
7430
8158
  if (ui.permissions) ui.permissions.sync()
7431
8159
  if (ui.model) ui.model.sync()
8160
+ if (ui.heroMenu) ui.heroMenu.sync()
7432
8161
  if (ui.footer) ui.footer.sync()
7433
8162
  if (ui.settings) ui.settings.sync()
7434
8163
  // Covers the rail toggle (and any reflow) while the popover is open:
@@ -7497,6 +8226,7 @@ window.__ModuleLoader__.load({
7497
8226
  document.removeEventListener('keydown', onGlobalKeyDown, true)
7498
8227
  document.removeEventListener('input', onComposerInput, true)
7499
8228
  document.removeEventListener('compositionend', onComposerInput, true)
8229
+ document.removeEventListener('focusin', onComposerFocusIn, true)
7500
8230
  // Safety-net DOM sweep. Feature teardowns run after this and tolerate
7501
8231
  // nodes already being detached.
7502
8232
  var leftoverItems = document.querySelectorAll('.dsh-claude-popover-item, .dsh-claude-popover-embed, .dsh-claude-account-popover, .dsh-claude-account-btn, .dsh-claude-perm-container, .dsh-claude-perm-popover, .dsh-claude-segments[data-composer-segments], .dsh-claude-model-btn, .dsh-claude-model-popover, .dsh-claude-ban, [data-dsh-synthetic-placeholder]')
@@ -7530,6 +8260,13 @@ window.__ModuleLoader__.load({
7530
8260
  * `.dsh-claude-segment` classes — the same control the composer's permission
7531
8261
  * picker uses — so the two read as one design instead of two lookalikes.
7532
8262
  */
8263
+ /**
8264
+ * The quick-provider popover (src/overrides/quick-providers.js). The settings
8265
+ * row is React-rendered while that card is imperative, so the row reaches the
8266
+ * popover through this handle.
8267
+ */
8268
+ var quickProviderApi = null
8269
+
7533
8270
  function ClaudeStyleSettingsSection() {
7534
8271
  var state = React.useState(readPrefs())
7535
8272
  var prefs = state[0]
@@ -7541,6 +8278,7 @@ window.__ModuleLoader__.load({
7541
8278
  var username = usernameState[0]
7542
8279
  var setUsername = usernameState[1]
7543
8280
  var usernameTimer = React.useRef(null)
8281
+ var quickTrigger = React.useRef(null)
7544
8282
 
7545
8283
  // The skin's own apply-side writes land here too (a reload, a conflict
7546
8284
  // re-read), so the page never drifts from what the document says.
@@ -7669,6 +8407,17 @@ window.__ModuleLoader__.load({
7669
8407
  { value: BAN_LOCALE_ZH, label: settingsCopy('banLocaleZh', '中文') },
7670
8408
  { value: BAN_LOCALE_EN, label: settingsCopy('banLocaleEn', 'English') },
7671
8409
  ]
8410
+ var autoPopoverOptions = [
8411
+ { value: AUTO_POPOVER_OFF, label: settingsCopy('autoPopoverOff', 'Off') },
8412
+ { value: AUTO_POPOVER_ACCOUNT, label: settingsCopy('autoPopoverAccount', 'Account only') },
8413
+ { value: AUTO_POPOVER_ALL, label: settingsCopy('autoPopoverAll', 'All') },
8414
+ ]
8415
+
8416
+ /** What the quick-provider trigger reads: how many, or nothing chosen. */
8417
+ var quickSummary = function (chosen) {
8418
+ if (chosen.length === 0) return settingsCopy('quickNone', 'None')
8419
+ return settingsCopy('quickCount', '{count} providers', { count: chosen.length })
8420
+ }
7672
8421
 
7673
8422
  var rows = [
7674
8423
  row(
@@ -7715,8 +8464,8 @@ window.__ModuleLoader__.load({
7715
8464
  row(
7716
8465
  'autoPopover',
7717
8466
  settingsCopy('autoPopoverTitle', 'Open popovers on hover'),
7718
- settingsCopy('autoPopoverDesc', 'Hover opens the account, model, and permission popovers. Off switches them to click-to-open.'),
7719
- toggle(prefs.autoPopover, function (value) { write({ autoPopover: value }) }),
8467
+ settingsCopy('autoPopoverDesc', 'Which popovers hover opens. Off leaves them all click-to-open.'),
8468
+ segment(autoPopoverOptions, prefs.autoPopover, function (value) { write({ autoPopover: value }) }),
7720
8469
  ),
7721
8470
  row(
7722
8471
  'composerScope',
@@ -7724,6 +8473,28 @@ window.__ModuleLoader__.load({
7724
8473
  settingsCopy('composerDesc', 'Which input area the skin restyles: the new-conversation page, the conversation, or both.'),
7725
8474
  segment(scopeOptions, prefs.composerScope, function (value) { write({ composerScope: value }) }),
7726
8475
  ),
8476
+ row(
8477
+ 'modelPicker',
8478
+ settingsCopy('pickerTitle', 'Redraw the model picker'),
8479
+ settingsCopy('pickerDesc', 'Replace the composer\'s model seat with the two-level Claude-style menu. Off hands the model menu back to the host; the rest of the composer restyle is unaffected.'),
8480
+ toggle(prefs.modelPicker, function (value) { write({ modelPicker: value }) }),
8481
+ ),
8482
+ row(
8483
+ 'quickProviders',
8484
+ settingsCopy('quickTitle', 'Quick providers'),
8485
+ settingsCopy('quickDesc', 'Providers whose models the picker\'s first level lists, one rule between providers. Nothing picked keeps the default: the official service.'),
8486
+ React.createElement('button', {
8487
+ type: 'button',
8488
+ ref: quickTrigger,
8489
+ className: 'dsh-claude-settings-picker',
8490
+ 'aria-haspopup': 'menu',
8491
+ 'aria-expanded': 'false',
8492
+ onClick: function () {
8493
+ if (quickProviderApi === null || quickTrigger.current === null) return
8494
+ quickProviderApi.toggle(quickTrigger.current, function (next) { write({ quickProviders: next }) })
8495
+ },
8496
+ }, quickSummary(prefs.quickProviders)),
8497
+ ),
7727
8498
  row(
7728
8499
  'banLocale',
7729
8500
  settingsCopy('banLocaleTitle', 'Account-hold easter egg language'),
@@ -7784,6 +8555,7 @@ window.__ModuleLoader__.load({
7784
8555
  ui.settings = {
7785
8556
  sync: syncSettingsNav,
7786
8557
  }
8558
+ quickProviderApi = ui.quickProviders || null
7787
8559
  }
7788
8560
  if (typeof ctx.inject !== 'function') return function () {}
7789
8561
  var fiber = ctx.inject(['slots'], function (scope) {
@@ -7809,6 +8581,7 @@ window.__ModuleLoader__.load({
7809
8581
  if (ui && ui.settings) {
7810
8582
  delete ui.settings
7811
8583
  }
8584
+ quickProviderApi = null
7812
8585
  try {
7813
8586
  if (fiber && typeof fiber.dispose === 'function') fiber.dispose()
7814
8587
  } catch (error) {
@@ -7844,6 +8617,8 @@ window.__ModuleLoader__.load({
7844
8617
  teardowns.push(installCopy(ctx, ui))
7845
8618
  teardowns.push(installPermissions(ctx, ui))
7846
8619
  teardowns.push(installModelPicker(ctx, ui))
8620
+ teardowns.push(installHeroMenu(ctx, ui)) // hero 行的目录/预设弹层:打标记给样式表用
8621
+ teardowns.push(installQuickProviders(ctx, ui)) // 设置页的「快捷供应商」多选弹层
7847
8622
  teardowns.push(installAccountFooter(ctx, ui))
7848
8623
  teardowns.push(installBanScreen(ctx, ui)) // 账户横条的封号彩蛋(账户弹层把点击交给 ui.ban)
7849
8624
  teardowns.push(installThemeFlip()) // 主题翻转瞬间抑制过渡,修掉「先色后样」