dsh-claude-style 0.3.2 → 0.5.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/CHANGELOG.md +62 -1
- package/LICENSE +0 -8
- package/README.en.md +5 -13
- package/README.md +5 -19
- package/docs/STYLE.md +82 -38
- package/docs/architecture.md +15 -7
- package/lib/claude-mark.svg +1 -0
- package/lib/client.js +2868 -373
- package/lib/index.js +213 -49
- package/lib/model-descriptions.json +97 -5
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -5
- package/fonts/GoogleSansFlexPicker.woff2 +0 -0
- package/fonts/OFL-GoogleSansFlex.txt +0 -93
package/lib/client.js
CHANGED
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
* - src/overrides/copy.js
|
|
17
17
|
* - src/overrides/permissions.js
|
|
18
18
|
* - src/overrides/model-brand.js
|
|
19
|
+
* - src/overrides/model-copy-lookup.js
|
|
20
|
+
* - src/overrides/model-effort.js
|
|
19
21
|
* - src/overrides/model-picker.js
|
|
22
|
+
* - src/overrides/hero-menu.js
|
|
23
|
+
* - src/overrides/quick-providers.js
|
|
20
24
|
* - src/overrides/account-footer.js
|
|
21
25
|
* - src/overrides/ban-screen.js
|
|
22
26
|
* - src/overrides/theme-flip.js
|
|
27
|
+
* - src/overrides/workspace-view.js
|
|
23
28
|
* - src/overrides/scheduler.js
|
|
24
29
|
* - src/settings.js
|
|
25
30
|
* - src/entry.js
|
|
@@ -36,6 +41,7 @@
|
|
|
36
41
|
* - src/styles/components/account-footer.css
|
|
37
42
|
* - src/styles/components/ban-screen.css
|
|
38
43
|
* - src/styles/components/model-picker.css
|
|
44
|
+
* - src/styles/components/hero-menu.css
|
|
39
45
|
* - src/styles/components/footer-takeover.css
|
|
40
46
|
* - src/styles/components/third-party.css
|
|
41
47
|
* - src/styles/components/settings.css
|
|
@@ -55,6 +61,25 @@ window.__ModuleLoader__.load({
|
|
|
55
61
|
|
|
56
62
|
var STYLE_ID = 'dsh-claude-style-style'
|
|
57
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Settings identity, for both host generations.
|
|
66
|
+
*
|
|
67
|
+
* 0.1.5-rc.2 and earlier let a plugin own a namespace by name
|
|
68
|
+
* (`claude-style`, registered host-side). 0.1.7 dropped that registry: a
|
|
69
|
+
* settings namespace IS a profile entry id and its schema IS the entry's
|
|
70
|
+
* Config, so the id below is what both halves address — read off the
|
|
71
|
+
* running loader entry where possible, with the id `cordis.patch.yml`
|
|
72
|
+
* inserts as the fallback.
|
|
73
|
+
*
|
|
74
|
+
* PACKAGE_NAME is the other half of the 0.1.7 contract: a bundle's own
|
|
75
|
+
* configuration is a `plugins.bundle.config` entry keyed by the bundle's
|
|
76
|
+
* package name, which is what makes it render on this plugin's page.
|
|
77
|
+
*/
|
|
78
|
+
var SETTINGS_ENTRY_FALLBACK = 'ui-skin-claude-style'
|
|
79
|
+
var PACKAGE_NAME = 'dsh-claude-style'
|
|
80
|
+
var BUNDLE_CONFIG_SLOT = 'plugins.bundle.config'
|
|
81
|
+
var SETTINGS_SECTION_SLOT = 'settings.section'
|
|
82
|
+
|
|
58
83
|
var COMPOSER_HINT = 'How can I help you today?'
|
|
59
84
|
|
|
60
85
|
/**
|
|
@@ -79,9 +104,14 @@ window.__ModuleLoader__.load({
|
|
|
79
104
|
var MODEL_EMPTY_LABEL = 'No models available.'
|
|
80
105
|
var MODEL_EFFORT_LABEL = 'Reasoning effort'
|
|
81
106
|
var MODEL_EFFORT_DEFAULT = 'Default'
|
|
107
|
+
/** The effort slider's two ends. Kept in English in every locale: they name
|
|
108
|
+
* the axis, not a level, and the level's own name rides beside the label. */
|
|
109
|
+
var MODEL_EFFORT_FASTER = 'Faster'
|
|
110
|
+
var MODEL_EFFORT_SMARTER = 'Smarter'
|
|
111
|
+
/** What the slider reads when the model offers no levels at all. */
|
|
112
|
+
var MODEL_EFFORT_NONE = '—'
|
|
82
113
|
var MODEL_MORE_LABEL = 'More models'
|
|
83
114
|
var MODEL_TRIGGER_LABEL = 'Select model, currently {model}'
|
|
84
|
-
var MODEL_NO_EFFORT_LABEL = 'This model offers no reasoning levels.'
|
|
85
115
|
|
|
86
116
|
/** English weekday names, indexed by Date#getDay() (0 = Sunday). */
|
|
87
117
|
var WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
|
@@ -220,6 +250,14 @@ window.__ModuleLoader__.load({
|
|
|
220
250
|
var DEFAULT_BAN_LOCALE = BAN_LOCALE_EN
|
|
221
251
|
/** Present while the composer restyle applies to the page currently shown. */
|
|
222
252
|
var COMPOSER_ATTR = 'data-dsh-claude-composer-active'
|
|
253
|
+
/**
|
|
254
|
+
* Stamped on the host's shared menu card while it is the hero row's picker
|
|
255
|
+
* (the workspace chip or the agent-preset seat opened it). The host portals
|
|
256
|
+
* that card to <body> with no marker of its own, so the stylesheet cannot
|
|
257
|
+
* tell it from the host's other menus; src/overrides/hero-menu.js stamps it
|
|
258
|
+
* and components/hero-menu.css switches on this attribute.
|
|
259
|
+
*/
|
|
260
|
+
var HERO_MENU_ATTR = 'data-dsh-claude-hero-menu'
|
|
223
261
|
/**
|
|
224
262
|
* Present while the browser window does NOT hold focus.
|
|
225
263
|
*
|
|
@@ -231,6 +269,16 @@ window.__ModuleLoader__.load({
|
|
|
231
269
|
var WINDOW_BLUR_ATTR = 'data-dsh-window-blur'
|
|
232
270
|
/** Composer surfaces the restyle may cover, in settings order. */
|
|
233
271
|
var COMPOSER_SCOPES = ['off', 'hero', 'conversation', 'all']
|
|
272
|
+
/**
|
|
273
|
+
* How eagerly the skin's popovers open on hover: `off` is click-only,
|
|
274
|
+
* `account` auto-opens the sidebar account popover alone, and `all` adds the
|
|
275
|
+
* permission, model, session-stats and the host's two hero-row pickers.
|
|
276
|
+
*/
|
|
277
|
+
var AUTO_POPOVER_OFF = 'off'
|
|
278
|
+
var AUTO_POPOVER_ACCOUNT = 'account'
|
|
279
|
+
var AUTO_POPOVER_ALL = 'all'
|
|
280
|
+
var AUTO_POPOVER_SCOPES = [AUTO_POPOVER_OFF, AUTO_POPOVER_ACCOUNT, AUTO_POPOVER_ALL]
|
|
281
|
+
var DEFAULT_AUTO_POPOVER = AUTO_POPOVER_ALL
|
|
234
282
|
/** Route the browser half reads and writes preferences through (lib/index.js). */
|
|
235
283
|
var PREFS_ROUTE = '/dsh-claude-style/prefs'
|
|
236
284
|
/** Route that resolves the host OS user once; never polled. */
|
|
@@ -263,13 +311,6 @@ window.__ModuleLoader__.load({
|
|
|
263
311
|
" --dsw-font-serif: 'Anthropic Serif Web Text',Georgia,'Times New Roman','Noto Sans SC','Source Han Sans SC','PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;",
|
|
264
312
|
" --dsw-font-prose: 'Anthropic Serif Web Text',Georgia,'Times New Roman','Noto Sans SC','Source Han Sans SC','PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;",
|
|
265
313
|
" --dsw-font-code: 'JetBrains Mono','Noto Sans SC','Source Han Sans SC','PingFang SC','Hiragino Sans GB','Microsoft YaHei',ui-monospace,'SF Mono','Fira Code',Consolas,'Liberation Mono',Menlo,Courier,monospace;",
|
|
266
|
-
" /* The picker's vendor face: a model row may wear the typeface of the vendor",
|
|
267
|
-
" that made the model, exactly as it already wears that vendor's mark. Only",
|
|
268
|
-
" Gemini has one today — a Latin-only Google Sans Flex subset (standard",
|
|
269
|
-
" weight, upright, SIL OFL 1.1, fonts/OFL-GoogleSansFlex.txt) — and the UI",
|
|
270
|
-
" face stays behind it in the stack, so a label carrying a character outside",
|
|
271
|
-
" the subset still renders instead of falling to tofu. */",
|
|
272
|
-
" --dsw-font-brand-gemini: 'Google Sans Flex Picker', var(--dsw-font-family);",
|
|
273
314
|
" /* Claude's lockup is drawn in Anthropic's serif, so its rows take the same face:",
|
|
274
315
|
" the wordmark and the rest of the label then read as one piece of lettering.",
|
|
275
316
|
" The face is already served for the UI (see typography.css); the serif token",
|
|
@@ -502,23 +543,6 @@ window.__ModuleLoader__.load({
|
|
|
502
543
|
" font-display: swap;",
|
|
503
544
|
"}",
|
|
504
545
|
"",
|
|
505
|
-
"/* The one vendor typeface the skin borrows: a Gemini row's name is set in",
|
|
506
|
-
" Google Sans Flex, so the row reads as Google's model rather than as one more",
|
|
507
|
-
" Claude-labelled entry — the row already carries the vendor's mark, and the",
|
|
508
|
-
" label is the only string in the picker that names a model instead of the",
|
|
509
|
-
" shell. It ships in the package as a Latin-only subset (standard weight,",
|
|
510
|
-
" upright, kerned; ~9 KB from a 4.2 MB six-axis variable font) built by",
|
|
511
|
-
" scripts/slim-google-sans.py under SIL OFL 1.1 — fonts/OFL-GoogleSansFlex.txt.",
|
|
512
|
-
" Latin-only is the point: characters outside the subset fall through to the",
|
|
513
|
-
" stack in --dsw-font-brand-gemini, which is why this is a static 400 cut. */",
|
|
514
|
-
"@font-face {",
|
|
515
|
-
" font-family: 'Google Sans Flex Picker';",
|
|
516
|
-
" src: url('/dsh-claude-style/fonts/GoogleSansFlexPicker.woff2') format('woff2');",
|
|
517
|
-
" font-weight: 400;",
|
|
518
|
-
" font-style: normal;",
|
|
519
|
-
" font-display: swap;",
|
|
520
|
-
"}",
|
|
521
|
-
"",
|
|
522
546
|
"/* ---------- typography: serif display + sans UI + mono code ---------- */",
|
|
523
547
|
"body[data-dsh-claude-style] {",
|
|
524
548
|
" font-family: var(--dsw-font-family);",
|
|
@@ -926,9 +950,17 @@ window.__ModuleLoader__.load({
|
|
|
926
950
|
" z-index: 100 !important;",
|
|
927
951
|
"}",
|
|
928
952
|
"",
|
|
953
|
+
"/* The header actions are NOT inside the composer seat, so unlike the three",
|
|
954
|
+
" overlays above they escape every stacking context and their value is read",
|
|
955
|
+
" against the page: the right Sidebar's panel covers through ui-dockkit's dock",
|
|
956
|
+
" layer (`--dsh-dockkit-dock-layer`, 10 normal and 40 fullscreen), and a",
|
|
957
|
+
" header action above those paints through the fullscreen panel. 9 keeps the",
|
|
958
|
+
" actions over the message layer (pinned headers 7, back-to-bottom 8) while",
|
|
959
|
+
" staying under both panel layers. The composer overlays keep 100 because the",
|
|
960
|
+
" seat's own stacking context caps them. */",
|
|
929
961
|
"body[data-dsh-claude-style] [data-slot=\"conversation.session.header.actions\"] > * {",
|
|
930
962
|
" position: relative;",
|
|
931
|
-
" z-index:
|
|
963
|
+
" z-index: 9 !important;",
|
|
932
964
|
"}",
|
|
933
965
|
"",
|
|
934
966
|
"/* The shipped queue dock cancels the stack gap AND pulls 3px further, so the",
|
|
@@ -1123,7 +1155,7 @@ window.__ModuleLoader__.load({
|
|
|
1123
1155
|
"",
|
|
1124
1156
|
"/* Commands and attach: 7px corners instead of the shipped full circle, no",
|
|
1125
1157
|
" resting fill, and a warm plate on hover. */",
|
|
1126
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]) {",
|
|
1158
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]) {",
|
|
1127
1159
|
" border-radius: 7px;",
|
|
1128
1160
|
"}",
|
|
1129
1161
|
"",
|
|
@@ -1133,11 +1165,11 @@ window.__ModuleLoader__.load({
|
|
|
1133
1165
|
" border-radius: 7px;",
|
|
1134
1166
|
"}",
|
|
1135
1167
|
"",
|
|
1136
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active]:not([data-ds-dark-theme]) :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]) {",
|
|
1168
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active]:not([data-ds-dark-theme]) :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]) {",
|
|
1137
1169
|
" background: transparent;",
|
|
1138
1170
|
"}",
|
|
1139
1171
|
"",
|
|
1140
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active]:not([data-ds-dark-theme]) :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]):hover:not(:disabled) {",
|
|
1172
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active]:not([data-ds-dark-theme]) :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]):hover:not(:disabled) {",
|
|
1141
1173
|
" background: #f6f6f4;",
|
|
1142
1174
|
"}",
|
|
1143
1175
|
"",
|
|
@@ -1642,8 +1674,8 @@ window.__ModuleLoader__.load({
|
|
|
1642
1674
|
" align-items: center !important;",
|
|
1643
1675
|
" justify-content: center !important;",
|
|
1644
1676
|
"}",
|
|
1645
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card][data-composer-variant=\"inline\"] :is(button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]),",
|
|
1646
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [class*=\"composerStack\"]:not([data-composer-variant=\"hero\"]) [data-composer-card] :is(button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]) {",
|
|
1677
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card][data-composer-variant=\"inline\"] :is(button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]),",
|
|
1678
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [class*=\"composerStack\"]:not([data-composer-variant=\"hero\"]) [data-composer-card] :is(button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]) {",
|
|
1647
1679
|
" order: 3 !important;",
|
|
1648
1680
|
" width: 24px !important;",
|
|
1649
1681
|
" height: 24px !important;",
|
|
@@ -1654,8 +1686,8 @@ window.__ModuleLoader__.load({
|
|
|
1654
1686
|
" align-items: center !important;",
|
|
1655
1687
|
" justify-content: center !important;",
|
|
1656
1688
|
"}",
|
|
1657
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card][data-composer-variant=\"inline\"] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]):hover:not(:disabled),",
|
|
1658
|
-
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [class*=\"composerStack\"]:not([data-composer-variant=\"hero\"]) [data-composer-card] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"]):hover:not(:disabled) {",
|
|
1689
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card][data-composer-variant=\"inline\"] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]):hover:not(:disabled),",
|
|
1690
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [class*=\"composerStack\"]:not([data-composer-variant=\"hero\"]) [data-composer-card] :is(button[aria-label=\"指令\"], button[aria-label=\"Commands\"], button[aria-label=\"添加附件\"], button[aria-label=\"Add attachment\"], button[aria-label*=\"添加文件\"], button[aria-label*=\"Add files\"]):hover:not(:disabled) {",
|
|
1659
1691
|
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
1660
1692
|
" color: var(--dsw-alias-label-primary) !important;",
|
|
1661
1693
|
"}",
|
|
@@ -1764,14 +1796,15 @@ window.__ModuleLoader__.load({
|
|
|
1764
1796
|
" z-index: 3 !important;",
|
|
1765
1797
|
" width: auto !important;",
|
|
1766
1798
|
" max-width: none !important;",
|
|
1767
|
-
"
|
|
1799
|
+
" /* Auto width, centered: the sentence hugs its own text (so the hover plate",
|
|
1800
|
+
" covers the text and not the whole row) and the auto margins centre it in",
|
|
1801
|
+
" the air between the left tool cluster and the trailing model group. */",
|
|
1802
|
+
" margin: 0 auto !important;",
|
|
1768
1803
|
" padding: 0 !important;",
|
|
1769
1804
|
" height: auto !important;",
|
|
1770
1805
|
" line-height: 20px !important;",
|
|
1771
|
-
" /* Fill the space between the left tool cluster and the trailing model",
|
|
1772
|
-
" group, then center the sentence in it: equal air on both sides. */",
|
|
1773
1806
|
" justify-content: center !important;",
|
|
1774
|
-
" flex:
|
|
1807
|
+
" flex: 0 1 auto !important;",
|
|
1775
1808
|
" min-width: 0 !important;",
|
|
1776
1809
|
"}",
|
|
1777
1810
|
"/* The stats pills are on-demand: hidden unless the chat window (the composer)",
|
|
@@ -1877,6 +1910,26 @@ window.__ModuleLoader__.load({
|
|
|
1877
1910
|
" border-radius: 6px !important;",
|
|
1878
1911
|
"}",
|
|
1879
1912
|
"",
|
|
1913
|
+
"/* ---------- one type for the composer's bottom line ---------- */",
|
|
1914
|
+
"/* The ring, the model trigger and the stats sentence are three host widgets",
|
|
1915
|
+
" that each brought their own type: the trigger 13px/500 in the secondary",
|
|
1916
|
+
" tone, the stats 12px/400 tertiary, the ring the host's secondary content",
|
|
1917
|
+
" size. They share one line now, so they read as one family — same face, same",
|
|
1918
|
+
" size, same weight, same tone. The stats' inner buttons inherit through the",
|
|
1919
|
+
" `font: inherit` rule above; the ring's trigger is targeted explicitly because",
|
|
1920
|
+
" the host sets its own colour and size on that button. */",
|
|
1921
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-context-meter],",
|
|
1922
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-context-meter] button,",
|
|
1923
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card] [class*=\"_row\"] [data-composer-stats],",
|
|
1924
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-composer-card] [class*=\"_row\"] .dsh-claude-model-btn {",
|
|
1925
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
1926
|
+
" font-size: 13px !important;",
|
|
1927
|
+
" font-weight: 500 !important;",
|
|
1928
|
+
" line-height: 20px !important;",
|
|
1929
|
+
" letter-spacing: normal !important;",
|
|
1930
|
+
" color: var(--dsw-alias-label-secondary) !important;",
|
|
1931
|
+
"}",
|
|
1932
|
+
"",
|
|
1880
1933
|
"/* The host renders two separate stat dialogs; hide them and use one custom",
|
|
1881
1934
|
" combined popover instead. */",
|
|
1882
1935
|
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [role=\"dialog\"]:has([data-session-stats-details]),",
|
|
@@ -1888,21 +1941,22 @@ window.__ModuleLoader__.load({
|
|
|
1888
1941
|
" content below, roomy type. */",
|
|
1889
1942
|
"body[data-dsh-claude-style][data-dsh-claude-composer-active] .dsh-claude-stats-popover {",
|
|
1890
1943
|
" position: fixed !important;",
|
|
1891
|
-
" z-index:
|
|
1892
|
-
" min-width:
|
|
1944
|
+
" z-index: 99999 !important;",
|
|
1945
|
+
" min-width: 248px !important;",
|
|
1893
1946
|
" max-width: min(380px, calc(100vw - 16px)) !important;",
|
|
1894
1947
|
" box-sizing: border-box !important;",
|
|
1895
|
-
" padding:
|
|
1948
|
+
" padding: 6px !important;",
|
|
1896
1949
|
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
1897
1950
|
" border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
1898
1951
|
" border-radius: 12px !important;",
|
|
1899
1952
|
" box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
|
|
1900
1953
|
" display: flex !important;",
|
|
1901
1954
|
" flex-direction: column !important;",
|
|
1902
|
-
" gap:
|
|
1955
|
+
" gap: 6px !important;",
|
|
1903
1956
|
" opacity: 0 !important;",
|
|
1904
1957
|
" pointer-events: none !important;",
|
|
1905
1958
|
" transform: translateY(4px) scale(0.98) !important;",
|
|
1959
|
+
" transform-origin: bottom left !important;",
|
|
1906
1960
|
" transition: opacity 0.15s ease, transform 0.15s ease !important;",
|
|
1907
1961
|
" font-family: var(--dsw-font-family) !important;",
|
|
1908
1962
|
"}",
|
|
@@ -1940,8 +1994,9 @@ window.__ModuleLoader__.load({
|
|
|
1940
1994
|
" display: flex !important;",
|
|
1941
1995
|
" flex-direction: column !important;",
|
|
1942
1996
|
" gap: 2px !important;",
|
|
1943
|
-
"
|
|
1944
|
-
"
|
|
1997
|
+
" min-height: 32px !important;",
|
|
1998
|
+
" padding: 2px 7px !important;",
|
|
1999
|
+
" border-radius: 6px !important;",
|
|
1945
2000
|
"}",
|
|
1946
2001
|
"body[data-dsh-claude-style][data-dsh-claude-composer-active] .dsh-claude-stats-popover-item:hover {",
|
|
1947
2002
|
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
@@ -2029,7 +2084,10 @@ window.__ModuleLoader__.load({
|
|
|
2029
2084
|
"}",
|
|
2030
2085
|
"",
|
|
2031
2086
|
"/* ---------- Claude Code layout: new session button ---------- */",
|
|
2032
|
-
"/* Narrow height matching conversation row,
|
|
2087
|
+
"/* Narrow height matching conversation row, left-aligned plus icon. No plate at",
|
|
2088
|
+
" rest — Claude's own sidebar keeps every row flat until the pointer is on it —",
|
|
2089
|
+
" and the leading glyph is a 20px CHIP: a circular fill with the plus drawn",
|
|
2090
|
+
" inside, which is how Claude marks its primary \"New\" row. */",
|
|
2033
2091
|
"body[data-dsh-claude-style] button[class*=\"newSession\"] {",
|
|
2034
2092
|
" display: flex !important;",
|
|
2035
2093
|
" align-items: center !important;",
|
|
@@ -2043,37 +2101,277 @@ window.__ModuleLoader__.load({
|
|
|
2043
2101
|
" margin: 0 0 6px 0 !important;",
|
|
2044
2102
|
" border: none !important;",
|
|
2045
2103
|
" border-radius: 6px !important;",
|
|
2046
|
-
" background:
|
|
2104
|
+
" background: transparent !important;",
|
|
2047
2105
|
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2048
2106
|
" font-family: var(--dsw-font-family) !important;",
|
|
2049
2107
|
" font-size: 13px !important;",
|
|
2050
2108
|
" font-weight: 500 !important;",
|
|
2109
|
+
" /* Explicit, so the label's x does not depend on the host's gap: both sidebar",
|
|
2110
|
+
" rows place their text at padding + 20px icon box + 6px. */",
|
|
2111
|
+
" gap: 6px !important;",
|
|
2051
2112
|
" cursor: pointer !important;",
|
|
2052
2113
|
" box-shadow: none !important;",
|
|
2053
2114
|
" transition: background-color 0.12s ease !important;",
|
|
2054
2115
|
"}",
|
|
2055
2116
|
"body[data-dsh-claude-style] button[class*=\"newSession\"]:hover {",
|
|
2056
|
-
" background: rgba(0, 0, 0, 0.
|
|
2117
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
2057
2118
|
"}",
|
|
2058
2119
|
"body[data-dsh-claude-style][data-ds-dark-theme] button[class*=\"newSession\"] {",
|
|
2059
2120
|
" color: #f5f4ef !important;",
|
|
2060
2121
|
"}",
|
|
2061
2122
|
"body[data-dsh-claude-style][data-ds-dark-theme] button[class*=\"newSession\"]:hover {",
|
|
2062
|
-
" background: rgba(255, 255, 255, 0.
|
|
2123
|
+
" background: rgba(255, 255, 255, 0.08) !important;",
|
|
2063
2124
|
"}",
|
|
2064
2125
|
"body[data-dsh-claude-style] button[class*=\"newSession\"] svg {",
|
|
2065
2126
|
" display: none !important;",
|
|
2066
2127
|
"}",
|
|
2128
|
+
"/* The chip: one 20px box that paints BOTH the circular fill and the plus. A mask",
|
|
2129
|
+
" cannot do that — it clips the element to the glyph, taking the fill with it —",
|
|
2130
|
+
" so the plus is a background image instead, which costs the currentColor",
|
|
2131
|
+
" adaptivity and buys a theme pair below. */",
|
|
2067
2132
|
"body[data-dsh-claude-style] button[class*=\"newSession\"]::before {",
|
|
2068
2133
|
" content: \"\";",
|
|
2069
2134
|
" display: inline-block !important;",
|
|
2135
|
+
" width: 20px !important;",
|
|
2136
|
+
" height: 20px !important;",
|
|
2137
|
+
" margin-right: 0 !important;",
|
|
2138
|
+
" flex: none !important;",
|
|
2139
|
+
" border-radius: 50% !important;",
|
|
2140
|
+
" /* The page inherits `corner-shape: superellipse(1.5)` from <html>, which turns",
|
|
2141
|
+
" a 50% radius into a pebble. True circles say so — exactly what the host's",
|
|
2142
|
+
" own spinner/dot/toggle rules do. */",
|
|
2143
|
+
" corner-shape: round !important;",
|
|
2144
|
+
" background-color: rgba(0, 0, 0, 0.10) !important;",
|
|
2145
|
+
" background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23141413' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='6' x2='12' y2='18'></line><line x1='6' y1='12' x2='18' y2='12'></line></svg>\") !important;",
|
|
2146
|
+
" background-repeat: no-repeat !important;",
|
|
2147
|
+
" background-position: center !important;",
|
|
2148
|
+
" background-size: 12px 12px !important;",
|
|
2149
|
+
" -webkit-mask: none !important;",
|
|
2150
|
+
" mask: none !important;",
|
|
2151
|
+
"}",
|
|
2152
|
+
"body[data-dsh-claude-style][data-ds-dark-theme] button[class*=\"newSession\"]::before {",
|
|
2153
|
+
" background-color: rgba(255, 255, 255, 0.14) !important;",
|
|
2154
|
+
" background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23faf9f5' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='6' x2='12' y2='18'></line><line x1='6' y1='12' x2='18' y2='12'></line></svg>\") !important;",
|
|
2155
|
+
"}",
|
|
2156
|
+
"",
|
|
2157
|
+
"/* ---------- Claude Code layout: the sidebar's panel row (插件) ---------- */",
|
|
2158
|
+
"/* DSH 0.1.7 added a panel entry right under the new-session button, styled by",
|
|
2159
|
+
" the host at 36px tall / 14px / 12px corners while the new-session row is",
|
|
2160
|
+
" 28px / 13px / 6px — two rows stacked on top of each other reading as two",
|
|
2161
|
+
" different lists. One row, one look: the same metrics, the same flat rest and",
|
|
2162
|
+
" hover plate as the new-session button, and the same 20px icon box so both",
|
|
2163
|
+
" labels start at the same x. */",
|
|
2164
|
+
"body[data-dsh-claude-style] button[class*=\"panelRow\"] {",
|
|
2165
|
+
" display: flex !important;",
|
|
2166
|
+
" align-items: center !important;",
|
|
2167
|
+
" justify-content: flex-start !important;",
|
|
2168
|
+
" text-align: left !important;",
|
|
2169
|
+
" width: 100% !important;",
|
|
2170
|
+
" box-sizing: border-box !important;",
|
|
2171
|
+
" height: 28px !important;",
|
|
2172
|
+
" /* The host floors this row at 36px with a min-height, which `height` alone",
|
|
2173
|
+
" cannot beat. */",
|
|
2174
|
+
" min-height: 28px !important;",
|
|
2175
|
+
" line-height: 28px !important;",
|
|
2176
|
+
" padding: 0 8px !important;",
|
|
2177
|
+
" margin: 0 0 6px 0 !important;",
|
|
2178
|
+
" border: none !important;",
|
|
2179
|
+
" border-radius: 6px !important;",
|
|
2180
|
+
" background: transparent !important;",
|
|
2181
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2182
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
2183
|
+
" font-size: 13px !important;",
|
|
2184
|
+
" font-weight: 500 !important;",
|
|
2185
|
+
" gap: 6px !important;",
|
|
2186
|
+
" cursor: pointer !important;",
|
|
2187
|
+
" box-shadow: none !important;",
|
|
2188
|
+
" transition: background-color 0.12s ease !important;",
|
|
2189
|
+
"}",
|
|
2190
|
+
"body[data-dsh-claude-style] button[class*=\"panelRow\"]:hover {",
|
|
2191
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
2192
|
+
"}",
|
|
2193
|
+
"body[data-dsh-claude-style][data-ds-dark-theme] button[class*=\"panelRow\"] {",
|
|
2194
|
+
" color: #f5f4ef !important;",
|
|
2195
|
+
"}",
|
|
2196
|
+
"body[data-dsh-claude-style][data-ds-dark-theme] button[class*=\"panelRow\"]:hover {",
|
|
2197
|
+
" background: rgba(255, 255, 255, 0.08) !important;",
|
|
2198
|
+
"}",
|
|
2199
|
+
"/* A 13px glyph in a 20px box: the same box the new-session chip occupies, so the",
|
|
2200
|
+
" two labels line up. */",
|
|
2201
|
+
"body[data-dsh-claude-style] button[class*=\"panelRow\"] svg {",
|
|
2070
2202
|
" width: 13px !important;",
|
|
2071
2203
|
" height: 13px !important;",
|
|
2072
|
-
" margin
|
|
2204
|
+
" margin: 0 3.5px !important;",
|
|
2205
|
+
" flex: none !important;",
|
|
2206
|
+
"}",
|
|
2207
|
+
"",
|
|
2208
|
+
"/* ---------- sidebar rows: the icon turns a quarter on hover ---------- */",
|
|
2209
|
+
"/* Both glyphs are four-fold symmetric, so the turn ends where it started and",
|
|
2210
|
+
" reads as a spin rather than a change of state. The transition is what turns",
|
|
2211
|
+
" the icon back when the pointer leaves. */",
|
|
2212
|
+
"body[data-dsh-claude-style] button[class*=\"newSession\"]::before,",
|
|
2213
|
+
"body[data-dsh-claude-style] button[class*=\"panelRow\"] svg {",
|
|
2214
|
+
" transition: transform 0.25s ease !important;",
|
|
2215
|
+
" transform-origin: center !important;",
|
|
2216
|
+
"}",
|
|
2217
|
+
"body[data-dsh-claude-style] button[class*=\"newSession\"]:hover::before,",
|
|
2218
|
+
"body[data-dsh-claude-style] button[class*=\"panelRow\"]:hover svg {",
|
|
2219
|
+
" transform: rotate(90deg) !important;",
|
|
2220
|
+
"}",
|
|
2221
|
+
"",
|
|
2222
|
+
"/* ---------- workspace section: 进行中 / 已归档 ---------- */",
|
|
2223
|
+
"/* The section label gives way to a two-state segment control (the skin's own",
|
|
2224
|
+
" markup, built by src/overrides/workspace-view.js); the archived state swaps",
|
|
2225
|
+
" the host's tree for the skin's list, and the list's rows carry a delete",
|
|
2226
|
+
" button. */",
|
|
2227
|
+
"body[data-dsh-claude-style] [class*=\"sectionLabel\"][data-dsh-claude-ws-label] {",
|
|
2228
|
+
" display: none !important;",
|
|
2229
|
+
"}",
|
|
2230
|
+
"body[data-dsh-claude-style] .dsh-claude-ws-segments {",
|
|
2231
|
+
" display: inline-flex !important;",
|
|
2232
|
+
" align-items: center !important;",
|
|
2233
|
+
" gap: 2px !important;",
|
|
2234
|
+
" padding: 2px !important;",
|
|
2235
|
+
" border-radius: 7px !important;",
|
|
2236
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.06)) !important;",
|
|
2237
|
+
" flex: none !important;",
|
|
2238
|
+
"}",
|
|
2239
|
+
"body[data-dsh-claude-style] .dsh-claude-ws-segment {",
|
|
2240
|
+
" appearance: none !important;",
|
|
2241
|
+
" border: none !important;",
|
|
2242
|
+
" background: transparent !important;",
|
|
2243
|
+
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
2244
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
2245
|
+
" font-size: 12px !important;",
|
|
2246
|
+
" font-weight: 500 !important;",
|
|
2247
|
+
" line-height: 18px !important;",
|
|
2248
|
+
" padding: 0 8px !important;",
|
|
2249
|
+
" height: 20px !important;",
|
|
2250
|
+
" border-radius: 5px !important;",
|
|
2251
|
+
" cursor: pointer !important;",
|
|
2252
|
+
" transition: background-color 0.12s ease, color 0.12s ease !important;",
|
|
2253
|
+
"}",
|
|
2254
|
+
"body[data-dsh-claude-style] .dsh-claude-ws-segment:hover {",
|
|
2255
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2256
|
+
"}",
|
|
2257
|
+
"body[data-dsh-claude-style] .dsh-claude-ws-segment[aria-checked=\"true\"] {",
|
|
2258
|
+
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
2259
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2260
|
+
" box-shadow: 0 1px 2px rgba(20, 20, 19, 0.10) !important;",
|
|
2261
|
+
"}",
|
|
2262
|
+
"body[data-dsh-claude-style][data-ds-dark-theme] .dsh-claude-ws-segment[aria-checked=\"true\"] {",
|
|
2263
|
+
" background: rgba(255, 255, 255, 0.14) !important;",
|
|
2264
|
+
" color: #f5f4ef !important;",
|
|
2265
|
+
" box-shadow: none !important;",
|
|
2266
|
+
"}",
|
|
2267
|
+
"/* ---------- workspace view: the skin's own archived list ---------- */",
|
|
2268
|
+
"/* 已归档 is the skin's list (src/overrides/workspace-view.js), flat on purpose:",
|
|
2269
|
+
" the host's own filter buries archived rows inside collapsed workspace groups",
|
|
2270
|
+
" and its state is not reachable without clicking its menu. */",
|
|
2271
|
+
"body[data-dsh-claude-style] [data-dsh-claude-ws-view=\"archived\"] [data-dsh-claude-ws-tree] {",
|
|
2272
|
+
" display: none !important;",
|
|
2273
|
+
"}",
|
|
2274
|
+
"body[data-dsh-claude-style] [data-dsh-claude-ws-view=\"active\"] .dsh-claude-archive-list {",
|
|
2275
|
+
" display: none !important;",
|
|
2276
|
+
"}",
|
|
2277
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-list {",
|
|
2278
|
+
" display: flex !important;",
|
|
2279
|
+
" flex-direction: column !important;",
|
|
2280
|
+
" gap: 1px !important;",
|
|
2281
|
+
" /* No inset of its own: the region already indents its content by 12px, which is",
|
|
2282
|
+
" where the host's tree rows start (measured: row x=12, title x=36). The 5px on",
|
|
2283
|
+
" the right is the tree's own inset — matching it keeps the two rows the same",
|
|
2284
|
+
" width (251px) instead of letting this one run 5px wider. */",
|
|
2285
|
+
" padding: 0 5px 0 0 !important;",
|
|
2286
|
+
" min-width: 0 !important;",
|
|
2287
|
+
"}",
|
|
2288
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-row {",
|
|
2289
|
+
" display: flex !important;",
|
|
2290
|
+
" align-items: center !important;",
|
|
2291
|
+
" gap: 4px !important;",
|
|
2292
|
+
" height: 28px !important;",
|
|
2293
|
+
" /* The host's own session row: 6px inside the list's 12px, so the row's box and",
|
|
2294
|
+
" the title's x land where the tree's rows put them (measured 12 / 36). */",
|
|
2295
|
+
" padding: 0 6px !important;",
|
|
2296
|
+
" border-radius: 6px !important;",
|
|
2297
|
+
" cursor: pointer !important;",
|
|
2298
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2299
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
2300
|
+
" font-size: 13px !important;",
|
|
2301
|
+
" font-weight: 400 !important;",
|
|
2302
|
+
" transition: background-color 0.12s ease !important;",
|
|
2303
|
+
"}",
|
|
2304
|
+
"/* The leading slot the host's rows carry (a 5px circle centred in 14px), so the",
|
|
2305
|
+
" two lists start their titles at the same x. */",
|
|
2306
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-row::before {",
|
|
2307
|
+
" content: \"\" !important;",
|
|
2308
|
+
" flex: none !important;",
|
|
2309
|
+
" width: 5px !important;",
|
|
2310
|
+
" height: 5px !important;",
|
|
2311
|
+
" margin: 0 4.5px !important;",
|
|
2312
|
+
" border-radius: 50% !important;",
|
|
2313
|
+
" corner-shape: round !important;",
|
|
2314
|
+
" border: 1.2px solid var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
2315
|
+
" box-sizing: border-box !important;",
|
|
2316
|
+
"}",
|
|
2317
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-row:hover {",
|
|
2318
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.06)) !important;",
|
|
2319
|
+
"}",
|
|
2320
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-title {",
|
|
2321
|
+
" flex: 1 1 auto !important;",
|
|
2322
|
+
" min-width: 0 !important;",
|
|
2323
|
+
" overflow: hidden !important;",
|
|
2324
|
+
" white-space: nowrap !important;",
|
|
2325
|
+
" text-overflow: ellipsis !important;",
|
|
2326
|
+
"}",
|
|
2327
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-time {",
|
|
2073
2328
|
" flex: none !important;",
|
|
2074
|
-
"
|
|
2075
|
-
" -
|
|
2076
|
-
"
|
|
2329
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
2330
|
+
" font-size: 11px !important;",
|
|
2331
|
+
"}",
|
|
2332
|
+
"/* The two row actions stay out of the way until the row is hovered: the list is",
|
|
2333
|
+
" for reading, and a column of buttons reads as a toolbar. Each button lives in",
|
|
2334
|
+
" the span its React root renders into. */",
|
|
2335
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-action {",
|
|
2336
|
+
" display: inline-flex !important;",
|
|
2337
|
+
" flex: none !important;",
|
|
2338
|
+
"}",
|
|
2339
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-delete,",
|
|
2340
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-restore {",
|
|
2341
|
+
" flex: none !important;",
|
|
2342
|
+
" display: inline-flex !important;",
|
|
2343
|
+
" align-items: center !important;",
|
|
2344
|
+
" justify-content: center !important;",
|
|
2345
|
+
" width: 22px !important;",
|
|
2346
|
+
" height: 22px !important;",
|
|
2347
|
+
" padding: 0 !important;",
|
|
2348
|
+
" border: none !important;",
|
|
2349
|
+
" border-radius: 5px !important;",
|
|
2350
|
+
" background: transparent !important;",
|
|
2351
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
2352
|
+
" cursor: pointer !important;",
|
|
2353
|
+
" opacity: 0 !important;",
|
|
2354
|
+
" transition: opacity 0.12s ease, background-color 0.12s ease, color 0.12s ease !important;",
|
|
2355
|
+
"}",
|
|
2356
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-row:hover .dsh-claude-archive-delete,",
|
|
2357
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-row:hover .dsh-claude-archive-restore,",
|
|
2358
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-delete:focus-visible,",
|
|
2359
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-restore:focus-visible {",
|
|
2360
|
+
" opacity: 1 !important;",
|
|
2361
|
+
"}",
|
|
2362
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-restore:hover {",
|
|
2363
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
2364
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
2365
|
+
"}",
|
|
2366
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-delete:hover {",
|
|
2367
|
+
" background: rgba(192, 57, 43, 0.12) !important;",
|
|
2368
|
+
" color: var(--dsw-alias-state-error-primary, #c0392b) !important;",
|
|
2369
|
+
"}",
|
|
2370
|
+
"body[data-dsh-claude-style] .dsh-claude-archive-status {",
|
|
2371
|
+
" padding: 8px !important;",
|
|
2372
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
2373
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
2374
|
+
" font-size: 12px !important;",
|
|
2077
2375
|
"}",
|
|
2078
2376
|
"",
|
|
2079
2377
|
"/* ---------- Claude Code layout: workspace & session sidebar ---------- */",
|
|
@@ -2238,6 +2536,7 @@ window.__ModuleLoader__.load({
|
|
|
2238
2536
|
" width: 5px;",
|
|
2239
2537
|
" height: 5px;",
|
|
2240
2538
|
" border-radius: 50%;",
|
|
2539
|
+
" corner-shape: round;",
|
|
2241
2540
|
" border: 1.2px solid var(--dsw-alias-label-caption, #a6a094);",
|
|
2242
2541
|
" box-sizing: border-box;",
|
|
2243
2542
|
"}",
|
|
@@ -2248,6 +2547,7 @@ window.__ModuleLoader__.load({
|
|
|
2248
2547
|
" height: 10px !important;",
|
|
2249
2548
|
" box-sizing: border-box !important;",
|
|
2250
2549
|
" border-radius: 50% !important;",
|
|
2550
|
+
" corner-shape: round !important;",
|
|
2251
2551
|
" border: 1.5px solid rgba(255, 255, 255, 0.18) !important;",
|
|
2252
2552
|
" border-top-color: #faf9f5 !important;",
|
|
2253
2553
|
" animation: 0.85s linear infinite dsh-claude-win11-spin !important;",
|
|
@@ -2403,7 +2703,7 @@ window.__ModuleLoader__.load({
|
|
|
2403
2703
|
"}",
|
|
2404
2704
|
"body[data-dsh-claude-style] .dsh-claude-perm-popover {",
|
|
2405
2705
|
" position: fixed !important;",
|
|
2406
|
-
" min-width:
|
|
2706
|
+
" min-width: 248px !important;",
|
|
2407
2707
|
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
2408
2708
|
" border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
2409
2709
|
" border-radius: 12px !important;",
|
|
@@ -2527,10 +2827,10 @@ window.__ModuleLoader__.load({
|
|
|
2527
2827
|
" box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
|
|
2528
2828
|
" padding: 6px !important;",
|
|
2529
2829
|
" box-sizing: border-box !important;",
|
|
2530
|
-
" z-index:
|
|
2830
|
+
" z-index: 99999 !important;",
|
|
2531
2831
|
" display: flex !important;",
|
|
2532
2832
|
" flex-direction: column !important;",
|
|
2533
|
-
" gap:
|
|
2833
|
+
" gap: 6px !important;",
|
|
2534
2834
|
" opacity: 0 !important;",
|
|
2535
2835
|
" pointer-events: none !important;",
|
|
2536
2836
|
" transform: translateY(6px) scale(0.98) !important;",
|
|
@@ -2563,8 +2863,9 @@ window.__ModuleLoader__.load({
|
|
|
2563
2863
|
" strip is the inner row, so the hover plate covers the name and not the divider",
|
|
2564
2864
|
" that follows it. */",
|
|
2565
2865
|
"body[data-dsh-claude-style] .dsh-claude-account-popover-row {",
|
|
2566
|
-
"
|
|
2567
|
-
"
|
|
2866
|
+
" min-height: 32px !important;",
|
|
2867
|
+
" padding: 2px 7px !important;",
|
|
2868
|
+
" border-radius: 6px !important;",
|
|
2568
2869
|
" cursor: pointer !important;",
|
|
2569
2870
|
" transition: background-color 0.12s ease !important;",
|
|
2570
2871
|
"}",
|
|
@@ -2903,6 +3204,7 @@ window.__ModuleLoader__.load({
|
|
|
2903
3204
|
" height: 24px;",
|
|
2904
3205
|
" margin-top: 1px;",
|
|
2905
3206
|
" border-radius: 50%;",
|
|
3207
|
+
" corner-shape: round;",
|
|
2906
3208
|
" background: var(--dsh-ban-hover);",
|
|
2907
3209
|
" color: var(--dsh-ban-text);",
|
|
2908
3210
|
" font-size: 12px;",
|
|
@@ -3050,7 +3352,11 @@ window.__ModuleLoader__.load({
|
|
|
3050
3352
|
" position: fixed !important;",
|
|
3051
3353
|
" min-width: 248px !important;",
|
|
3052
3354
|
" max-width: min(360px, calc(100vw - 16px)) !important;",
|
|
3053
|
-
"
|
|
3355
|
+
" /* The first level is sized by its content like the second: the cap is only",
|
|
3356
|
+
" the viewport, so a short list does not leave a tall empty card. The level-1",
|
|
3357
|
+
" list scrolls inside the body instead (see the scoped rule below); this",
|
|
3358
|
+
" overflow-y stays for the sub popover, whose whole card scrolls. */",
|
|
3359
|
+
" max-height: calc(100vh - 80px) !important;",
|
|
3054
3360
|
" overflow-y: auto !important;",
|
|
3055
3361
|
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
3056
3362
|
" border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
@@ -3084,17 +3390,35 @@ window.__ModuleLoader__.load({
|
|
|
3084
3390
|
" gap: 3px !important;",
|
|
3085
3391
|
" min-width: 0 !important;",
|
|
3086
3392
|
"}",
|
|
3393
|
+
"/* Level 1 splits into a scrollable list and a pinned footer: the model rows",
|
|
3394
|
+
" scroll under the divider / effort / more-models rows, which never leave the",
|
|
3395
|
+
" viewport edge of the card. Only the first level does this — the sub popover",
|
|
3396
|
+
" keeps scrolling as one card, with its provider labels pinned (below). */",
|
|
3397
|
+
"body[data-dsh-claude-style] .dsh-claude-model-popover:not(.dsh-claude-model-popover-sub) {",
|
|
3398
|
+
" overflow: hidden !important;",
|
|
3399
|
+
"}",
|
|
3400
|
+
"body[data-dsh-claude-style] .dsh-claude-model-popover:not(.dsh-claude-model-popover-sub) .dsh-claude-model-popover-body {",
|
|
3401
|
+
" flex: 1 1 auto !important;",
|
|
3402
|
+
" min-height: 0 !important;",
|
|
3403
|
+
" overflow-y: auto !important;",
|
|
3404
|
+
"}",
|
|
3405
|
+
"body[data-dsh-claude-style] .dsh-claude-model-footer {",
|
|
3406
|
+
" flex: none !important;",
|
|
3407
|
+
" display: flex !important;",
|
|
3408
|
+
" flex-direction: column !important;",
|
|
3409
|
+
" gap: 2px !important;",
|
|
3410
|
+
" min-width: 0 !important;",
|
|
3411
|
+
"}",
|
|
3087
3412
|
"body[data-dsh-claude-style] .dsh-claude-model-status {",
|
|
3088
3413
|
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
3089
3414
|
" font-size: 12px !important;",
|
|
3090
3415
|
" line-height: 20px !important;",
|
|
3091
3416
|
" padding: 4px 8px !important;",
|
|
3092
3417
|
"}",
|
|
3093
|
-
"/* The sub popover is taller than the first level
|
|
3094
|
-
"
|
|
3095
|
-
"
|
|
3418
|
+
"/* The sub popover is taller than the first level and sized by its content: the",
|
|
3419
|
+
" cap is only the viewport, so a short provider list does not leave a tall empty",
|
|
3420
|
+
" card. Provider labels stay pinned to the top while the list scrolls. */",
|
|
3096
3421
|
"body[data-dsh-claude-style] .dsh-claude-model-popover-sub {",
|
|
3097
|
-
" max-height: min(540px, calc(100vh - 80px)) !important;",
|
|
3098
3422
|
" padding-top: 0 !important;",
|
|
3099
3423
|
"}",
|
|
3100
3424
|
"",
|
|
@@ -3102,7 +3426,9 @@ window.__ModuleLoader__.load({
|
|
|
3102
3426
|
" position: relative !important;",
|
|
3103
3427
|
" display: flex !important;",
|
|
3104
3428
|
" flex-direction: column !important;",
|
|
3105
|
-
"
|
|
3429
|
+
" /* Level 2 rows sit close together: the 3px the body uses between sections",
|
|
3430
|
+
" read as a gap between models of the same provider. */",
|
|
3431
|
+
" gap: 1px !important;",
|
|
3106
3432
|
" min-width: 0 !important;",
|
|
3107
3433
|
"}",
|
|
3108
3434
|
"",
|
|
@@ -3171,16 +3497,6 @@ window.__ModuleLoader__.load({
|
|
|
3171
3497
|
" font-weight: 500 !important;",
|
|
3172
3498
|
" line-height: 16px !important;",
|
|
3173
3499
|
"}",
|
|
3174
|
-
"/* A Gemini row wears Gemini's own face for its name. The row already carries the",
|
|
3175
|
-
" vendor's mark, and the name is the one string in the picker that names a model",
|
|
3176
|
-
" rather than the shell — so the vendor's typeface is the same signal in a second",
|
|
3177
|
-
" material. `data-brand` is the mark id the picker stamps on the row",
|
|
3178
|
-
" (src/overrides/model-picker.js); only Gemini resolves to a face today, and a",
|
|
3179
|
-
" brand without one simply keeps the UI face. The stack is additive: a name with",
|
|
3180
|
-
" a character outside the Latin subset falls through character by character. */",
|
|
3181
|
-
"body[data-dsh-claude-style] .dsh-claude-model-option[data-brand='gemini'] .dsh-claude-model-name {",
|
|
3182
|
-
" font-family: var(--dsw-font-brand-gemini) !important;",
|
|
3183
|
-
"}",
|
|
3184
3500
|
"/* Claude's wordmark is set in Anthropic's serif, so the rest of the label follows",
|
|
3185
3501
|
" it — the row reads as one piece of lettering rather than serif art beside sans. */",
|
|
3186
3502
|
"body[data-dsh-claude-style] .dsh-claude-model-option[data-brand='claude'] .dsh-claude-model-name {",
|
|
@@ -3243,13 +3559,43 @@ window.__ModuleLoader__.load({
|
|
|
3243
3559
|
" align-items: center !important;",
|
|
3244
3560
|
" color: var(--dsw-alias-brand-primary, #d97757) !important;",
|
|
3245
3561
|
"}",
|
|
3246
|
-
"
|
|
3562
|
+
"/* The picker's two hairlines share one declaration: the provider header's rule",
|
|
3563
|
+
" and the divider that closes the model list. Same colour, same thickness, same",
|
|
3564
|
+
" insets — the only difference left is that the header's line starts after the",
|
|
3565
|
+
" provider's name, which is the point of that row. */",
|
|
3566
|
+
"body[data-dsh-claude-style] .dsh-claude-model-divider,",
|
|
3567
|
+
"body[data-dsh-claude-style] .dsh-claude-model-rule::after {",
|
|
3247
3568
|
" height: 1px !important;",
|
|
3248
3569
|
" background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3570
|
+
"}",
|
|
3571
|
+
"body[data-dsh-claude-style] .dsh-claude-model-divider {",
|
|
3572
|
+
" margin: 2px 4px !important;",
|
|
3573
|
+
" flex: none !important;",
|
|
3574
|
+
"}",
|
|
3575
|
+
"/* The provider header: the name first, then the rule running to the right edge.",
|
|
3576
|
+
" The line is what is left of that row, not a separate full-width rule above it",
|
|
3577
|
+
" — one rule per provider boundary, and nothing more. An empty label leaves the",
|
|
3578
|
+
" bare rule, which is how a provider that needs no naming (the official source)",
|
|
3579
|
+
" is separated. */",
|
|
3580
|
+
"body[data-dsh-claude-style] .dsh-claude-model-rule {",
|
|
3581
|
+
" display: flex !important;",
|
|
3582
|
+
" align-items: center !important;",
|
|
3583
|
+
" gap: 6px !important;",
|
|
3249
3584
|
" margin: 2px 4px !important;",
|
|
3250
3585
|
" flex: none !important;",
|
|
3251
3586
|
"}",
|
|
3252
|
-
"
|
|
3587
|
+
"body[data-dsh-claude-style] .dsh-claude-model-rule::after {",
|
|
3588
|
+
" content: \"\" !important;",
|
|
3589
|
+
" flex: 1 !important;",
|
|
3590
|
+
"}",
|
|
3591
|
+
"body[data-dsh-claude-style] .dsh-claude-model-rule-name {",
|
|
3592
|
+
" flex: none !important;",
|
|
3593
|
+
" font-size: 11px !important;",
|
|
3594
|
+
" line-height: 14px !important;",
|
|
3595
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
3596
|
+
" letter-spacing: 0.02em !important;",
|
|
3597
|
+
"}",
|
|
3598
|
+
"/* Level-2 row: label + chevron; hover opens its own level. */",
|
|
3253
3599
|
"body[data-dsh-claude-style] .dsh-claude-model-cell {",
|
|
3254
3600
|
" display: flex !important;",
|
|
3255
3601
|
" align-items: center !important;",
|
|
@@ -3273,11 +3619,6 @@ window.__ModuleLoader__.load({
|
|
|
3273
3619
|
" flex: 1 !important;",
|
|
3274
3620
|
" min-width: 0 !important;",
|
|
3275
3621
|
"}",
|
|
3276
|
-
"body[data-dsh-claude-style] .dsh-claude-model-cell-value {",
|
|
3277
|
-
" flex: none !important;",
|
|
3278
|
-
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
3279
|
-
" font-size: 12px !important;",
|
|
3280
|
-
"}",
|
|
3281
3622
|
"body[data-dsh-claude-style] .dsh-claude-model-cell-chevron {",
|
|
3282
3623
|
" flex: none !important;",
|
|
3283
3624
|
" display: inline-flex !important;",
|
|
@@ -3285,42 +3626,151 @@ window.__ModuleLoader__.load({
|
|
|
3285
3626
|
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
3286
3627
|
"}",
|
|
3287
3628
|
"",
|
|
3288
|
-
"/*
|
|
3289
|
-
"
|
|
3290
|
-
"
|
|
3291
|
-
"
|
|
3292
|
-
"
|
|
3629
|
+
"/* ---------- Reasoning-effort slider (first level's footer) ---------- */",
|
|
3630
|
+
"/* The effort ladder drawn as a track instead of a second level. The knob",
|
|
3631
|
+
" travels continuously with the pointer and settles on the nearest level when",
|
|
3632
|
+
" the gesture ends — release, or the pointer leaving the control — so the",
|
|
3633
|
+
" control never reads as a hard switch between fixed stops. The transition is",
|
|
3634
|
+
" what makes that settle read as a settle; it is switched off while the pointer",
|
|
3635
|
+
" owns the knob, which is what `data-dragging` marks. */",
|
|
3636
|
+
"body[data-dsh-claude-style] .dsh-claude-effort {",
|
|
3293
3637
|
" display: flex !important;",
|
|
3294
3638
|
" flex-direction: column !important;",
|
|
3295
|
-
" gap:
|
|
3639
|
+
" gap: 3px !important;",
|
|
3640
|
+
" padding: 3px 7px 4px 7px !important;",
|
|
3641
|
+
" min-width: 0 !important;",
|
|
3642
|
+
" user-select: none !important;",
|
|
3643
|
+
" -webkit-user-select: none !important;",
|
|
3296
3644
|
"}",
|
|
3297
|
-
"body[data-dsh-claude-style] .dsh-claude-
|
|
3298
|
-
"
|
|
3299
|
-
"
|
|
3300
|
-
"
|
|
3301
|
-
"
|
|
3302
|
-
" font-family: var(--dsw-font-family) !important;",
|
|
3645
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-head {",
|
|
3646
|
+
" display: flex !important;",
|
|
3647
|
+
" align-items: baseline !important;",
|
|
3648
|
+
" gap: 6px !important;",
|
|
3649
|
+
" min-width: 0 !important;",
|
|
3303
3650
|
"}",
|
|
3304
|
-
"body[data-dsh-claude-style] .dsh-claude-
|
|
3305
|
-
" height: 1px !important;",
|
|
3306
|
-
" background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3307
|
-
" margin: 2px 0 !important;",
|
|
3651
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-label {",
|
|
3308
3652
|
" flex: none !important;",
|
|
3653
|
+
" font-size: 13px !important;",
|
|
3654
|
+
" line-height: 16px !important;",
|
|
3655
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
3309
3656
|
"}",
|
|
3310
|
-
"",
|
|
3311
|
-
"
|
|
3312
|
-
"
|
|
3657
|
+
"/* The level's name trails the label instead of sitting at the far edge: the",
|
|
3658
|
+
" control reads as one phrase, `Reasoning effort Default`, the way the ends",
|
|
3659
|
+
" below read as one scale. */",
|
|
3660
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-value {",
|
|
3661
|
+
" flex: 0 1 auto !important;",
|
|
3662
|
+
" min-width: 0 !important;",
|
|
3663
|
+
" overflow: hidden !important;",
|
|
3664
|
+
" text-overflow: ellipsis !important;",
|
|
3665
|
+
" white-space: nowrap !important;",
|
|
3666
|
+
" font-size: 13px !important;",
|
|
3667
|
+
" line-height: 16px !important;",
|
|
3668
|
+
" color: var(--dsw-alias-label-secondary, #787672) !important;",
|
|
3669
|
+
"}",
|
|
3670
|
+
"/* The ends name the axis, so they stay in English in every locale and in the",
|
|
3671
|
+
" caption colour: they are a scale, not a value. */",
|
|
3672
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-ends {",
|
|
3313
3673
|
" display: flex !important;",
|
|
3314
|
-
"
|
|
3315
|
-
"
|
|
3316
|
-
"
|
|
3317
|
-
"
|
|
3674
|
+
" align-items: center !important;",
|
|
3675
|
+
" justify-content: space-between !important;",
|
|
3676
|
+
" gap: 8px !important;",
|
|
3677
|
+
" font-size: 11px !important;",
|
|
3678
|
+
" line-height: 14px !important;",
|
|
3679
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
3318
3680
|
"}",
|
|
3319
|
-
"body[data-dsh-claude-style] .dsh-claude-
|
|
3320
|
-
"
|
|
3321
|
-
" height:
|
|
3681
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-track {",
|
|
3682
|
+
" position: relative !important;",
|
|
3683
|
+
" height: 18px !important;",
|
|
3684
|
+
" margin-top: 1px !important;",
|
|
3685
|
+
" cursor: pointer !important;",
|
|
3686
|
+
" touch-action: none !important;",
|
|
3687
|
+
"}",
|
|
3688
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-track::before {",
|
|
3689
|
+
" content: \"\" !important;",
|
|
3690
|
+
" position: absolute !important;",
|
|
3691
|
+
" left: 0 !important;",
|
|
3692
|
+
" right: 0 !important;",
|
|
3693
|
+
" top: 50% !important;",
|
|
3694
|
+
" /* Nearly as tall as the 16px knob (Claude's real Effort slider reads as one",
|
|
3695
|
+
" chunky pill, not a bead on a wire): 6px read as a thin rail the knob sat",
|
|
3696
|
+
" ON, 14px reads as a groove the knob rides IN. */",
|
|
3697
|
+
" height: 14px !important;",
|
|
3698
|
+
" margin-top: -7px !important;",
|
|
3699
|
+
" border-radius: 999px !important;",
|
|
3700
|
+
" /* A pill: its caps are meant to be round, and the page inherits superellipse",
|
|
3701
|
+
" corners from <html>. */",
|
|
3702
|
+
" corner-shape: round !important;",
|
|
3703
|
+
" background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08)) !important;",
|
|
3704
|
+
"}",
|
|
3705
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-knob {",
|
|
3706
|
+
" position: absolute !important;",
|
|
3707
|
+
" left: 0 !important;",
|
|
3708
|
+
" top: 50% !important;",
|
|
3709
|
+
" width: 16px !important;",
|
|
3710
|
+
" height: 16px !important;",
|
|
3711
|
+
" margin-top: -8px !important;",
|
|
3712
|
+
" border-radius: 50% !important;",
|
|
3713
|
+
" corner-shape: round !important;",
|
|
3714
|
+
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
3715
|
+
" box-shadow: 0 1px 3px rgba(20, 20, 19, 0.28), 0 0 0 1px rgba(20, 20, 19, 0.06) !important;",
|
|
3716
|
+
" transition: transform 0.18s cubic-bezier(0.22, 0.61, 0.36, 1) !important;",
|
|
3717
|
+
" will-change: transform !important;",
|
|
3718
|
+
"}",
|
|
3719
|
+
"body[data-dsh-claude-style][data-ds-dark-theme] .dsh-claude-effort-knob {",
|
|
3720
|
+
" box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4) !important;",
|
|
3721
|
+
"}",
|
|
3722
|
+
"body[data-dsh-claude-style] .dsh-claude-effort[data-dragging] .dsh-claude-effort-knob {",
|
|
3723
|
+
" transition: none !important;",
|
|
3724
|
+
"}",
|
|
3725
|
+
"body[data-dsh-claude-style] .dsh-claude-effort-track:focus-visible {",
|
|
3726
|
+
" outline: 2px solid var(--dsw-alias-brand-primary, #d97757) !important;",
|
|
3727
|
+
" outline-offset: 1px !important;",
|
|
3728
|
+
" border-radius: 6px !important;",
|
|
3729
|
+
"}",
|
|
3730
|
+
"/* No ladder to climb: the track is dimmed and the value reads as a dash, but the",
|
|
3731
|
+
" knob still travels and eases home — the control keeps its shape. */",
|
|
3732
|
+
"body[data-dsh-claude-style] .dsh-claude-effort[data-empty] .dsh-claude-effort-track::before {",
|
|
3733
|
+
" opacity: 0.55 !important;",
|
|
3734
|
+
"}",
|
|
3735
|
+
"body[data-dsh-claude-style] .dsh-claude-effort[data-empty] .dsh-claude-effort-value {",
|
|
3736
|
+
" color: var(--dsw-alias-label-caption, #a6a094) !important;",
|
|
3737
|
+
"}",
|
|
3738
|
+
"",
|
|
3739
|
+
"/* Popover Header. Vertical padding only: the clickable account row carries its",
|
|
3740
|
+
" own inline padding (components/account-footer.css), and the divider below is a",
|
|
3741
|
+
" full-bleed rule that must not be inset by this box. */",
|
|
3742
|
+
"body[data-dsh-claude-style] .dsh-claude-account-popover-header {",
|
|
3743
|
+
" padding: 6px 0 !important;",
|
|
3744
|
+
" display: flex !important;",
|
|
3745
|
+
" flex-direction: column !important;",
|
|
3746
|
+
" gap: 2px !important;",
|
|
3747
|
+
"}",
|
|
3748
|
+
"body[data-dsh-claude-style] .dsh-claude-account-popover-name {",
|
|
3749
|
+
" font-size: 14px !important;",
|
|
3750
|
+
" font-weight: 600 !important;",
|
|
3751
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
3752
|
+
" line-height: 18px !important;",
|
|
3753
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
3754
|
+
"}",
|
|
3755
|
+
"body[data-dsh-claude-style] .dsh-claude-account-popover-divider {",
|
|
3756
|
+
" height: 1px !important;",
|
|
3757
|
+
" background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3758
|
+
" margin: 2px 0 !important;",
|
|
3759
|
+
" flex: none !important;",
|
|
3760
|
+
"}",
|
|
3761
|
+
"",
|
|
3762
|
+
"/* Popover Body & Items */",
|
|
3763
|
+
"body[data-dsh-claude-style] .dsh-claude-account-popover-body {",
|
|
3764
|
+
" display: flex !important;",
|
|
3765
|
+
" flex-direction: column !important;",
|
|
3766
|
+
" gap: 2px !important;",
|
|
3767
|
+
" max-height: 360px !important;",
|
|
3768
|
+
" overflow-y: auto !important;",
|
|
3769
|
+
"}",
|
|
3770
|
+
"body[data-dsh-claude-style] .dsh-claude-popover-item {",
|
|
3771
|
+
" width: 100% !important;",
|
|
3322
3772
|
" min-height: 32px !important;",
|
|
3323
|
-
" padding:
|
|
3773
|
+
" padding: 2px 7px !important;",
|
|
3324
3774
|
" border-radius: 6px !important;",
|
|
3325
3775
|
" border: none !important;",
|
|
3326
3776
|
" background: transparent !important;",
|
|
@@ -3378,12 +3828,156 @@ window.__ModuleLoader__.load({
|
|
|
3378
3828
|
" background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08)) !important;",
|
|
3379
3829
|
" color: var(--dsw-alias-label-secondary, #787672) !important;",
|
|
3380
3830
|
"}",
|
|
3831
|
+
"/* A provider the catalog no longer offers: its stored row stays checked but is",
|
|
3832
|
+
" muted, so it reads as \"this used to be picked\" and can be unchecked away. */",
|
|
3833
|
+
"body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-text,",
|
|
3834
|
+
"body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-badge {",
|
|
3835
|
+
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
3836
|
+
"}",
|
|
3837
|
+
"body[data-dsh-claude-style] .dsh-claude-popover-item.dsh-claude-model-stale .dsh-claude-popover-item-badge {",
|
|
3838
|
+
" background: transparent !important;",
|
|
3839
|
+
"}",
|
|
3381
3840
|
"",
|
|
3382
3841
|
"/* Ensure popover items only display inside the popover */",
|
|
3383
3842
|
"body[data-dsh-claude-style] [class*=\"footArea\"] > .dsh-claude-popover-item {",
|
|
3384
3843
|
" display: none !important;",
|
|
3385
3844
|
"}",
|
|
3386
3845
|
"",
|
|
3846
|
+
"/* @composer-gate */",
|
|
3847
|
+
"/* ---------- Host menu primitive: the hero row's pickers ---------- */",
|
|
3848
|
+
"/* The workspace (directory) chip and the agent-preset seat open the host's",
|
|
3849
|
+
" shared Menu primitive, which portals its card to <body>. The card carries no",
|
|
3850
|
+
" marker of its own, so src/overrides/hero-menu.js stamps the one that belongs",
|
|
3851
|
+
" to this row with HERO_MENU_ATTR and these rules reach it alone — the host's",
|
|
3852
|
+
" other menus keep their own design. The metrics are the skin's popover",
|
|
3853
|
+
" language, the same card / rows / hover plate / accent check as",
|
|
3854
|
+
" components/model-picker.css and components/permissions.css. */",
|
|
3855
|
+
"",
|
|
3856
|
+
"/* Two class-name families meet here. The host's client-ui packages hash as",
|
|
3857
|
+
" `<hash>_<local>`, so their fragments read `_itemName` / `_itemDesc`; the",
|
|
3858
|
+
" shared menu ships inside the web shell and hashes as `_<local>_<hash>_<n>`,",
|
|
3859
|
+
" so its fragments read `_itemWrap_` / `_itemLabel_`. Both are the longest",
|
|
3860
|
+
" stable piece of the class — never the bare local name. */",
|
|
3861
|
+
"",
|
|
3862
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] {",
|
|
3863
|
+
" min-width: 248px !important;",
|
|
3864
|
+
" max-width: min(360px, calc(100vw - 16px)) !important;",
|
|
3865
|
+
" max-height: min(440px, calc(100vh - 96px)) !important;",
|
|
3866
|
+
" padding: 6px !important;",
|
|
3867
|
+
" gap: 6px !important;",
|
|
3868
|
+
" border: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3869
|
+
" border-radius: 12px !important;",
|
|
3870
|
+
" background: var(--dsw-alias-bg-overlay, #ffffff) !important;",
|
|
3871
|
+
" box-shadow: 0 8px 30px rgba(20, 20, 19, 0.12), 0 2px 8px rgba(20, 20, 19, 0.06) !important;",
|
|
3872
|
+
" z-index: 99999 !important;",
|
|
3873
|
+
" font-family: var(--dsw-font-family) !important;",
|
|
3874
|
+
" /* The host mounts the card instead of toggling a `data-open` attribute, so",
|
|
3875
|
+
" the skin's open transition becomes a one-shot animation with the same",
|
|
3876
|
+
" values. Its origin is the edge nearest the trigger: the card sits beside it. */",
|
|
3877
|
+
" transform-origin: bottom left !important;",
|
|
3878
|
+
" animation: dsh-claude-hero-menu-in 0.15s ease !important;",
|
|
3879
|
+
"}",
|
|
3880
|
+
"@keyframes dsh-claude-hero-menu-in {",
|
|
3881
|
+
" from {",
|
|
3882
|
+
" opacity: 0;",
|
|
3883
|
+
" transform: translateY(4px) scale(0.98);",
|
|
3884
|
+
" }",
|
|
3885
|
+
" to {",
|
|
3886
|
+
" opacity: 1;",
|
|
3887
|
+
" transform: none;",
|
|
3888
|
+
" }",
|
|
3889
|
+
"}",
|
|
3890
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active][data-ds-dark-theme] [data-dsh-claude-hero-menu] {",
|
|
3891
|
+
" background: #1e1e1d !important;",
|
|
3892
|
+
" border-color: #2e2c29 !important;",
|
|
3893
|
+
" box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3) !important;",
|
|
3894
|
+
"}",
|
|
3895
|
+
"",
|
|
3896
|
+
"/* One row height, one radius, one hover plate for both pickers. */",
|
|
3897
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"] {",
|
|
3898
|
+
" min-height: 32px !important;",
|
|
3899
|
+
" padding: 2px 7px !important;",
|
|
3900
|
+
" gap: 8px !important;",
|
|
3901
|
+
" border-radius: 6px !important;",
|
|
3902
|
+
" font-size: 13px !important;",
|
|
3903
|
+
" line-height: 20px !important;",
|
|
3904
|
+
" color: var(--dsw-alias-label-primary, #141413) !important;",
|
|
3905
|
+
" transition: background-color 0.12s ease !important;",
|
|
3906
|
+
"}",
|
|
3907
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"]:hover:not(:disabled) {",
|
|
3908
|
+
" background: var(--dsh-claude-hover-bg, rgba(0, 0, 0, 0.08)) !important;",
|
|
3909
|
+
"}",
|
|
3910
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemWrap_\"] > [role=\"menuitem\"]:disabled {",
|
|
3911
|
+
" opacity: 0.4 !important;",
|
|
3912
|
+
" cursor: not-allowed !important;",
|
|
3913
|
+
"}",
|
|
3914
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemIcon_\"] {",
|
|
3915
|
+
" width: 16px !important;",
|
|
3916
|
+
" height: 16px !important;",
|
|
3917
|
+
" color: var(--dsw-alias-label-secondary, #787672) !important;",
|
|
3918
|
+
"}",
|
|
3919
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemIcon_\"] svg {",
|
|
3920
|
+
" width: 16px !important;",
|
|
3921
|
+
" height: 16px !important;",
|
|
3922
|
+
"}",
|
|
3923
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemLabel_\"] {",
|
|
3924
|
+
" font-size: 13px !important;",
|
|
3925
|
+
" line-height: 20px !important;",
|
|
3926
|
+
"}",
|
|
3927
|
+
"/* The current row is marked by the accent, as in the model picker. */",
|
|
3928
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_check_\"] {",
|
|
3929
|
+
" flex: none !important;",
|
|
3930
|
+
" color: var(--dsw-alias-brand-primary, #d97757) !important;",
|
|
3931
|
+
"}",
|
|
3932
|
+
"",
|
|
3933
|
+
"/* The preset rows are two lines (name + description) and the workspace rows",
|
|
3934
|
+
" one with an icon, so the height above is a floor rather than a cap. */",
|
|
3935
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemName\"] {",
|
|
3936
|
+
" font-size: 13px !important;",
|
|
3937
|
+
" font-weight: 500 !important;",
|
|
3938
|
+
" line-height: 16px !important;",
|
|
3939
|
+
"}",
|
|
3940
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_itemDesc\"] {",
|
|
3941
|
+
" font-size: 11px !important;",
|
|
3942
|
+
" line-height: 14px !important;",
|
|
3943
|
+
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
3944
|
+
"}",
|
|
3945
|
+
"",
|
|
3946
|
+
"/* Heading and separator rows. */",
|
|
3947
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] > [class*=\"_viewport_\"] > [role=\"presentation\"] {",
|
|
3948
|
+
" padding: 6px 7px 2px !important;",
|
|
3949
|
+
" font-size: 11px !important;",
|
|
3950
|
+
" font-weight: 600 !important;",
|
|
3951
|
+
" line-height: 16px !important;",
|
|
3952
|
+
" letter-spacing: 0.04em !important;",
|
|
3953
|
+
" text-transform: uppercase !important;",
|
|
3954
|
+
" color: var(--dsw-alias-label-tertiary, #8f8d84) !important;",
|
|
3955
|
+
"}",
|
|
3956
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [role=\"separator\"] {",
|
|
3957
|
+
" height: 1px !important;",
|
|
3958
|
+
" margin: 2px 4px !important;",
|
|
3959
|
+
" background: var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3960
|
+
"}",
|
|
3961
|
+
"",
|
|
3962
|
+
"/* The pinned footer (\"添加工作区…\") keeps the host's spacing with the skin's",
|
|
3963
|
+
" hairline. */",
|
|
3964
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] > [class*=\"_footer_\"] {",
|
|
3965
|
+
" margin-top: 4px !important;",
|
|
3966
|
+
" padding-top: 4px !important;",
|
|
3967
|
+
" border-top: 1px solid var(--dsw-alias-border-l1, #e8e6dc) !important;",
|
|
3968
|
+
"}",
|
|
3969
|
+
"",
|
|
3970
|
+
"/* The card's padding is the skin's 6px rather than the host's 4px, so a side",
|
|
3971
|
+
" card moves with it: 6px pad + 6px gap. */",
|
|
3972
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_submenu_\"] {",
|
|
3973
|
+
" bottom: -6px !important;",
|
|
3974
|
+
" left: calc(100% + 12px) !important;",
|
|
3975
|
+
"}",
|
|
3976
|
+
"body[data-dsh-claude-style][data-dsh-claude-composer-active] [data-dsh-claude-hero-menu] [class*=\"_submenu_\"]::before {",
|
|
3977
|
+
" left: -12px !important;",
|
|
3978
|
+
" width: 12px !important;",
|
|
3979
|
+
"}",
|
|
3980
|
+
"",
|
|
3387
3981
|
"/* ---------- Footer takeover ----------",
|
|
3388
3982
|
" Everything in this block modifies the HOST's own footer (it hides entries and",
|
|
3389
3983
|
" zeroes the containers that host them) and exists only because the skin",
|
|
@@ -3680,6 +4274,8 @@ window.__ModuleLoader__.load({
|
|
|
3680
4274
|
" width: 16px;",
|
|
3681
4275
|
" height: 16px;",
|
|
3682
4276
|
" border-radius: 50%;",
|
|
4277
|
+
" /* A true circle: the page inherits superellipse corners from <html>. */",
|
|
4278
|
+
" corner-shape: round;",
|
|
3683
4279
|
" background: var(--dsw-alias-bg-overlay, #ffffff);",
|
|
3684
4280
|
" box-shadow: 0 1px 2px rgba(20, 20, 19, 0.18);",
|
|
3685
4281
|
" transition: transform .12s ease;",
|
|
@@ -3715,6 +4311,32 @@ window.__ModuleLoader__.load({
|
|
|
3715
4311
|
" color: var(--dsw-alias-label-caption);",
|
|
3716
4312
|
"}",
|
|
3717
4313
|
"",
|
|
4314
|
+
"/* The quick-provider trigger belongs to the same control family as the username",
|
|
4315
|
+
" input: one bordered plate that opens a picker. Its card is the shared popover",
|
|
4316
|
+
" card (components/permissions.css), so only the trigger is styled here. */",
|
|
4317
|
+
"body[data-dsh-claude-style] .dsh-claude-settings-picker {",
|
|
4318
|
+
" flex: none;",
|
|
4319
|
+
" min-width: 140px;",
|
|
4320
|
+
" max-width: 45%;",
|
|
4321
|
+
" box-sizing: border-box;",
|
|
4322
|
+
" border: 1px solid var(--dsw-alias-border-l1);",
|
|
4323
|
+
" border-radius: 8px;",
|
|
4324
|
+
" background: var(--dsw-alias-bg-layer-1);",
|
|
4325
|
+
" color: var(--dsw-alias-label-primary);",
|
|
4326
|
+
" padding: 6px 10px;",
|
|
4327
|
+
" font: inherit;",
|
|
4328
|
+
" font-size: 13px;",
|
|
4329
|
+
" line-height: 18px;",
|
|
4330
|
+
" text-align: left;",
|
|
4331
|
+
" cursor: pointer;",
|
|
4332
|
+
"}",
|
|
4333
|
+
"body[data-dsh-claude-style] .dsh-claude-settings-picker:hover {",
|
|
4334
|
+
" border-color: var(--dsw-alias-border-l2);",
|
|
4335
|
+
"}",
|
|
4336
|
+
"body[data-dsh-claude-style] .dsh-claude-settings-picker[aria-expanded=\"true\"] {",
|
|
4337
|
+
" border-color: var(--dsw-alias-brand-primary, #d97757);",
|
|
4338
|
+
"}",
|
|
4339
|
+
"",
|
|
3718
4340
|
"/* ============================================================================",
|
|
3719
4341
|
" 主题翻转:瞬时抑制过渡 (Theme Flip: Transition Suppression)",
|
|
3720
4342
|
" ============================================================================ */",
|
|
@@ -3907,11 +4529,20 @@ window.__ModuleLoader__.load({
|
|
|
3907
4529
|
/**
|
|
3908
4530
|
* Skin preferences.
|
|
3909
4531
|
*
|
|
3910
|
-
* The authoritative store is the host settings namespace
|
|
3911
|
-
*
|
|
3912
|
-
*
|
|
3913
|
-
*
|
|
3914
|
-
*
|
|
4532
|
+
* The authoritative store is the host settings namespace, reached one of two
|
|
4533
|
+
* ways depending on the host generation:
|
|
4534
|
+
*
|
|
4535
|
+
* * 0.1.7+ serves every registered namespace to the browser through
|
|
4536
|
+
* `ctx.configForms`, whose per-entry controller carries the values, the
|
|
4537
|
+
* write queue and the revision fence. That is the transport used
|
|
4538
|
+
* whenever the service is present.
|
|
4539
|
+
* * 0.1.5-rc.2 and earlier expose only the namespaces the api-proxy lists,
|
|
4540
|
+
* and a plugin's own is not among them, so this side falls back to the
|
|
4541
|
+
* route its host half registers (`lib/index.js`).
|
|
4542
|
+
*
|
|
4543
|
+
* Both transports carry the same eight fields, so everything below stays
|
|
4544
|
+
* transport-agnostic: `loadPrefs`/`savePrefs` pick one, and the rest of the
|
|
4545
|
+
* skin keeps reading the mirrored `prefs` object.
|
|
3915
4546
|
*
|
|
3916
4547
|
* Every value is mirrored onto the document as an attribute, so the
|
|
3917
4548
|
* stylesheet — not this module — decides what a preference means visually.
|
|
@@ -3968,8 +4599,10 @@ window.__ModuleLoader__.load({
|
|
|
3968
4599
|
var prefs = {
|
|
3969
4600
|
brand: DEFAULT_BRAND,
|
|
3970
4601
|
collapseFooter: true,
|
|
3971
|
-
autoPopover:
|
|
4602
|
+
autoPopover: DEFAULT_AUTO_POPOVER,
|
|
3972
4603
|
composerScope: 'all',
|
|
4604
|
+
modelPicker: true,
|
|
4605
|
+
quickProviders: [],
|
|
3973
4606
|
username: '',
|
|
3974
4607
|
banLocale: fallbackBanLocale || DEFAULT_BAN_LOCALE,
|
|
3975
4608
|
}
|
|
@@ -3977,6 +4610,84 @@ window.__ModuleLoader__.load({
|
|
|
3977
4610
|
var prefsAvailable = false
|
|
3978
4611
|
var prefsListeners = []
|
|
3979
4612
|
|
|
4613
|
+
/**
|
|
4614
|
+
* The official settings form, when this host has one.
|
|
4615
|
+
*
|
|
4616
|
+
* 0.1.7 replaced the imperative namespace registry with Config-derived
|
|
4617
|
+
* forms: `ctx.configForms.get(entryId)` hands back a controller carrying the
|
|
4618
|
+
* values, a write queue and a revision fence, and the namespace is this
|
|
4619
|
+
* plugin's profile entry id. The older host has no such service, so this
|
|
4620
|
+
* stays null there and the route below is the transport instead.
|
|
4621
|
+
*/
|
|
4622
|
+
var prefsForm = null
|
|
4623
|
+
|
|
4624
|
+
/** This plugin's settings namespace: the loader entry id, or the patch's id. */
|
|
4625
|
+
function settingsEntryId(ctx) {
|
|
4626
|
+
try {
|
|
4627
|
+
var entry = ctx && ctx.fiber ? ctx.fiber.entry : null
|
|
4628
|
+
var id = entry ? entry.id : null
|
|
4629
|
+
if (typeof id === 'string' && id !== '') return id
|
|
4630
|
+
} catch (error) { /* no loader entry: fall back to the id the patch declares */ }
|
|
4631
|
+
return SETTINGS_ENTRY_FALLBACK
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
/** Whether this host serves namespaces to the browser (0.1.7+). */
|
|
4635
|
+
function hostConfigForms(ctx) {
|
|
4636
|
+
try {
|
|
4637
|
+
if (!ctx || typeof ctx.get !== 'function') return null
|
|
4638
|
+
var forms = ctx.get('configForms')
|
|
4639
|
+
return forms !== null && forms !== undefined && typeof forms.get === 'function' ? forms : null
|
|
4640
|
+
} catch (error) {
|
|
4641
|
+
return null
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
/** The form's current field values, or null while it is not ready. */
|
|
4646
|
+
function readFormValue() {
|
|
4647
|
+
if (prefsForm === null) return null
|
|
4648
|
+
try {
|
|
4649
|
+
var snapshot = prefsForm.getSnapshot()
|
|
4650
|
+
if (snapshot === null || snapshot === undefined) return null
|
|
4651
|
+
if (snapshot.status !== 'ready') return null
|
|
4652
|
+
return snapshot.value && typeof snapshot.value === 'object' ? snapshot.value : null
|
|
4653
|
+
} catch (error) {
|
|
4654
|
+
return null
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
/**
|
|
4659
|
+
* Bind the official form when the host offers one.
|
|
4660
|
+
*
|
|
4661
|
+
* Called once per install, before the first read. A host that serves the
|
|
4662
|
+
* service later (or never) simply keeps the route transport, so this never
|
|
4663
|
+
* blocks or fails the skin.
|
|
4664
|
+
*/
|
|
4665
|
+
function adoptSettingsForm(ctx) {
|
|
4666
|
+
if (prefsForm !== null) return true
|
|
4667
|
+
var forms = hostConfigForms(ctx)
|
|
4668
|
+
if (forms === null) return false
|
|
4669
|
+
var form = null
|
|
4670
|
+
try {
|
|
4671
|
+
form = forms.get(settingsEntryId(ctx))
|
|
4672
|
+
} catch (error) {
|
|
4673
|
+
form = null
|
|
4674
|
+
}
|
|
4675
|
+
if (form === null || form === undefined || typeof form.getSnapshot !== 'function') return false
|
|
4676
|
+
prefsForm = form
|
|
4677
|
+
if (typeof form.subscribe === 'function') {
|
|
4678
|
+
try {
|
|
4679
|
+
form.subscribe(function () {
|
|
4680
|
+
var value = readFormValue()
|
|
4681
|
+
if (value === null) return
|
|
4682
|
+
prefsAvailable = true
|
|
4683
|
+
adoptPrefs(normalizePrefs(value))
|
|
4684
|
+
replayPendingBanLocale(value)
|
|
4685
|
+
})
|
|
4686
|
+
} catch (error) { /* no subscribe face: reads stay on demand */ }
|
|
4687
|
+
}
|
|
4688
|
+
return true
|
|
4689
|
+
}
|
|
4690
|
+
|
|
3980
4691
|
/**
|
|
3981
4692
|
* Browser-local fallback for the custom username.
|
|
3982
4693
|
*
|
|
@@ -4040,8 +4751,23 @@ window.__ModuleLoader__.load({
|
|
|
4040
4751
|
/**
|
|
4041
4752
|
* Read the preferences once. A failure keeps the defaults and leaves the
|
|
4042
4753
|
* settings page to report that the store is unavailable.
|
|
4754
|
+
*
|
|
4755
|
+
* The official form is read first when it exists: its subscription already
|
|
4756
|
+
* re-reads on every host change, so this call only has to cover the case
|
|
4757
|
+
* where the values are ready before the subscription settles.
|
|
4043
4758
|
*/
|
|
4044
4759
|
function loadPrefs() {
|
|
4760
|
+
if (prefsForm !== null) {
|
|
4761
|
+
var formValue = readFormValue()
|
|
4762
|
+
if (formValue !== null) {
|
|
4763
|
+
prefsAvailable = true
|
|
4764
|
+
var formName = typeof formValue.username === 'string' ? formValue.username.trim() : ''
|
|
4765
|
+
if (formName) setFallbackUsername('')
|
|
4766
|
+
adoptPrefs(normalizePrefs(formValue))
|
|
4767
|
+
replayPendingBanLocale(formValue)
|
|
4768
|
+
}
|
|
4769
|
+
return
|
|
4770
|
+
}
|
|
4045
4771
|
if (typeof fetch !== 'function') return
|
|
4046
4772
|
try {
|
|
4047
4773
|
fetch(PREFS_ROUTE, {
|
|
@@ -4066,14 +4792,46 @@ window.__ModuleLoader__.load({
|
|
|
4066
4792
|
} catch (error) { /* no fetch: defaults stay */ }
|
|
4067
4793
|
}
|
|
4068
4794
|
|
|
4795
|
+
/**
|
|
4796
|
+
* Clamp the hover-open preference. It used to be a boolean, and a value
|
|
4797
|
+
* stored in that shape still has to land on a scope: `true` meant every
|
|
4798
|
+
* popover, `false` meant click-only.
|
|
4799
|
+
*/
|
|
4800
|
+
function normalizeAutoPopover(value) {
|
|
4801
|
+
if (value === true) return AUTO_POPOVER_ALL
|
|
4802
|
+
if (value === false) return AUTO_POPOVER_OFF
|
|
4803
|
+
return AUTO_POPOVER_SCOPES.indexOf(value) === -1 ? DEFAULT_AUTO_POPOVER : value
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
/**
|
|
4807
|
+
* The provider ids the picker's first level carries. Ids rather than names:
|
|
4808
|
+
* a provider can be renamed by the catalog at any time, and the stored
|
|
4809
|
+
* selection has to survive that. Order is the caller's, duplicates dropped.
|
|
4810
|
+
* The official service is the picker's default, not a choice, so a stored
|
|
4811
|
+
* id for it is dropped: the first level shows it whenever nothing else is
|
|
4812
|
+
* picked, which is what "default" means.
|
|
4813
|
+
*/
|
|
4814
|
+
function normalizeQuickProviders(value) {
|
|
4815
|
+
if (!Array.isArray(value)) return []
|
|
4816
|
+
var out = []
|
|
4817
|
+
for (var i = 0; i < value.length; i++) {
|
|
4818
|
+
var id = value[i]
|
|
4819
|
+
if (typeof id !== 'string' || id === '' || id === MODEL_OFFICIAL_GROUP || out.indexOf(id) !== -1) continue
|
|
4820
|
+
out.push(id)
|
|
4821
|
+
}
|
|
4822
|
+
return out
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4069
4825
|
/** Clamp one host value into the preference shape (the host already did this). */
|
|
4070
4826
|
function normalizePrefs(value) {
|
|
4071
4827
|
var section = value && typeof value === 'object' ? value : {}
|
|
4072
4828
|
return {
|
|
4073
4829
|
brand: section.brand === BRAND_ANTHROPIC || section.brand === BRAND_OFF ? section.brand : BRAND_CLAUDE,
|
|
4074
4830
|
collapseFooter: section.collapseFooter !== false,
|
|
4075
|
-
autoPopover: section.autoPopover
|
|
4831
|
+
autoPopover: normalizeAutoPopover(section.autoPopover),
|
|
4076
4832
|
composerScope: COMPOSER_SCOPES.indexOf(section.composerScope) === -1 ? 'all' : section.composerScope,
|
|
4833
|
+
modelPicker: section.modelPicker !== false,
|
|
4834
|
+
quickProviders: normalizeQuickProviders(section.quickProviders),
|
|
4077
4835
|
username: (typeof section.username === 'string' ? section.username.trim().slice(0, USERNAME_MAX) : '') || fallbackUsername,
|
|
4078
4836
|
banLocale: resolveBanLocale(section.banLocale),
|
|
4079
4837
|
}
|
|
@@ -4098,6 +4856,65 @@ window.__ModuleLoader__.load({
|
|
|
4098
4856
|
savePrefs({ banLocale: fallbackBanLocale })
|
|
4099
4857
|
}
|
|
4100
4858
|
|
|
4859
|
+
/**
|
|
4860
|
+
* Write a partial change through the official form.
|
|
4861
|
+
*
|
|
4862
|
+
* One `set()` per field, chained: the controller owns the write queue and
|
|
4863
|
+
* takes its revision fence from the last settlement, so a burst of toggles
|
|
4864
|
+
* cannot interleave or lose a field. `set()` also validates the field path
|
|
4865
|
+
* against the entry's Config before anything crosses the wire, which is why
|
|
4866
|
+
* an unknown key is dropped here rather than sent.
|
|
4867
|
+
*
|
|
4868
|
+
* @param patch - preference keys to change.
|
|
4869
|
+
* @returns a promise for the resolved preferences, or null when refused.
|
|
4870
|
+
*/
|
|
4871
|
+
function savePrefsViaForm(patch) {
|
|
4872
|
+
var keys = []
|
|
4873
|
+
for (var key in patch) {
|
|
4874
|
+
if (Object.prototype.hasOwnProperty.call(patch, key)) keys.push(key)
|
|
4875
|
+
}
|
|
4876
|
+
var step = function (name) {
|
|
4877
|
+
return function (accepted) {
|
|
4878
|
+
if (accepted === false) return false
|
|
4879
|
+
try {
|
|
4880
|
+
var pending = prefsForm.set(name, patch[name])
|
|
4881
|
+
return pending && typeof pending.then === 'function'
|
|
4882
|
+
? pending.then(function (ok) { return ok === true })
|
|
4883
|
+
: true
|
|
4884
|
+
} catch (error) {
|
|
4885
|
+
return false
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
var run = Promise.resolve(true)
|
|
4890
|
+
for (var i = 0; i < keys.length; i++) run = run.then(step(keys[i]))
|
|
4891
|
+
return run.then(function (accepted) {
|
|
4892
|
+
if (accepted === false) {
|
|
4893
|
+
// Refused (a stale revision, or a field this Config does not carry):
|
|
4894
|
+
// re-read rather than guess, the way the route path does.
|
|
4895
|
+
loadPrefs()
|
|
4896
|
+
return null
|
|
4897
|
+
}
|
|
4898
|
+
var value = readFormValue()
|
|
4899
|
+
if (value !== null) {
|
|
4900
|
+
prefsAvailable = true
|
|
4901
|
+
if (typeof patch.username === 'string') {
|
|
4902
|
+
var hostName = typeof value.username === 'string' ? value.username.trim() : ''
|
|
4903
|
+
setFallbackUsername(hostName ? '' : patch.username)
|
|
4904
|
+
}
|
|
4905
|
+
// The host echoing the value back is the only proof it knows the
|
|
4906
|
+
// field; anything else means the write did not land and the local
|
|
4907
|
+
// fallback has to keep it.
|
|
4908
|
+
if (typeof patch.banLocale === 'string') {
|
|
4909
|
+
var hostLocale = typeof value.banLocale === 'string' ? value.banLocale : ''
|
|
4910
|
+
setFallbackBanLocale(hostLocale === patch.banLocale ? '' : patch.banLocale)
|
|
4911
|
+
}
|
|
4912
|
+
adoptPrefs(normalizePrefs(value))
|
|
4913
|
+
}
|
|
4914
|
+
return prefs
|
|
4915
|
+
})
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4101
4918
|
/**
|
|
4102
4919
|
* Write a partial preference change.
|
|
4103
4920
|
*
|
|
@@ -4109,6 +4926,7 @@ window.__ModuleLoader__.load({
|
|
|
4109
4926
|
* @returns a promise for the resolved preferences, or null when unavailable.
|
|
4110
4927
|
*/
|
|
4111
4928
|
function savePrefs(patch) {
|
|
4929
|
+
if (prefsForm !== null) return savePrefsViaForm(patch)
|
|
4112
4930
|
if (typeof fetch !== 'function') return Promise.resolve(null)
|
|
4113
4931
|
var body = { revision: prefsRevision }
|
|
4114
4932
|
for (var key in patch) body[key] = patch[key]
|
|
@@ -4151,19 +4969,6 @@ window.__ModuleLoader__.load({
|
|
|
4151
4969
|
})
|
|
4152
4970
|
}
|
|
4153
4971
|
|
|
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
4972
|
/**
|
|
4168
4973
|
* Model & settings localized copy.
|
|
4169
4974
|
*
|
|
@@ -4326,9 +5131,13 @@ window.__ModuleLoader__.load({
|
|
|
4326
5131
|
* the picker copy, so the page follows the shell language too — and the
|
|
4327
5132
|
* English constants stay as the fallback for a failed fetch.
|
|
4328
5133
|
*/
|
|
4329
|
-
function settingsCopy(key, fallback) {
|
|
5134
|
+
function settingsCopy(key, fallback, params) {
|
|
4330
5135
|
var text = modelCopy === null || !modelCopy.settings ? '' : localized(modelCopy.settings[key])
|
|
4331
|
-
|
|
5136
|
+
if (!text) text = fallback
|
|
5137
|
+
if (!params) return text
|
|
5138
|
+
return text.replace(/\{(\w+)\}/g, function (match, name) {
|
|
5139
|
+
return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match
|
|
5140
|
+
})
|
|
4332
5141
|
}
|
|
4333
5142
|
|
|
4334
5143
|
/**
|
|
@@ -4386,36 +5195,75 @@ window.__ModuleLoader__.load({
|
|
|
4386
5195
|
y = rect.top - (opts.gap || 0) - height
|
|
4387
5196
|
if (y < margin) y = Math.min(rect.bottom + (opts.gap || 0), Math.max(margin, window.innerHeight - height - margin))
|
|
4388
5197
|
}
|
|
5198
|
+
// Same-value guard: this runs on every scheduler pass while a popover is
|
|
5199
|
+
// open, and an identical write still dirties layout — the next geometry
|
|
5200
|
+
// read (the drag paths read rect/offset every frame) would then force a
|
|
5201
|
+
// synchronous recalc. Skip the write when the anchor did not move.
|
|
5202
|
+
var leftValue = Math.round(x) + 'px'
|
|
5203
|
+
var topValue = Math.round(y) + 'px'
|
|
4389
5204
|
if (opts.important) {
|
|
4390
|
-
pop.style.setProperty('left',
|
|
4391
|
-
pop.style.setProperty('top',
|
|
5205
|
+
if (pop.style.left !== leftValue) pop.style.setProperty('left', leftValue, 'important')
|
|
5206
|
+
if (pop.style.top !== topValue) pop.style.setProperty('top', topValue, 'important')
|
|
4392
5207
|
} else {
|
|
4393
|
-
pop.style.left
|
|
4394
|
-
pop.style.top
|
|
5208
|
+
if (pop.style.left !== leftValue) pop.style.left = leftValue
|
|
5209
|
+
if (pop.style.top !== topValue) pop.style.top = topValue
|
|
4395
5210
|
}
|
|
4396
5211
|
return { x: x, y: y }
|
|
4397
5212
|
}
|
|
4398
5213
|
|
|
4399
5214
|
/**
|
|
4400
|
-
* Hover
|
|
5215
|
+
* Hover dwell before a popover unfolds, and grace before it closes.
|
|
5216
|
+
*
|
|
5217
|
+
* The dwell is deliberately tiny: it exists to swallow a pointer that merely
|
|
5218
|
+
* CROSSES a trigger on its way somewhere else, not to make the user wait for
|
|
5219
|
+
* the card. The grace is what lets the pointer travel the gap between a
|
|
5220
|
+
* trigger and its card without the card vanishing underneath it.
|
|
5221
|
+
*/
|
|
5222
|
+
var POPOVER_OPEN_DELAY = 50
|
|
5223
|
+
var POPOVER_CLOSE_DELAY = 100
|
|
5224
|
+
|
|
5225
|
+
/**
|
|
5226
|
+
* Hover-intent helper shared by the model picker, permission popover and
|
|
5227
|
+
* account popover.
|
|
4401
5228
|
*
|
|
4402
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
*
|
|
4405
|
-
*
|
|
5229
|
+
* BOTH sides are scheduled: `scheduleOpen` waits out the dwell (so a pointer
|
|
5230
|
+
* crossing the trigger never unfolds anything) and `scheduleClose` waits out
|
|
5231
|
+
* the grace. `cancel` clears whichever side is pending — entering the card
|
|
5232
|
+
* cancels a close, and leaving the trigger before the dwell cancels the open
|
|
5233
|
+
* (`scheduleClose` drops a pending open as well, so a leave needs only the
|
|
5234
|
+
* one call).
|
|
4406
5235
|
*/
|
|
4407
|
-
function createHoverIntent(open, close,
|
|
4408
|
-
var
|
|
5236
|
+
function createHoverIntent(open, close, openDelay, closeDelay) {
|
|
5237
|
+
var openTimer = null
|
|
5238
|
+
var closeTimer = null
|
|
4409
5239
|
return {
|
|
4410
5240
|
cancel: function () {
|
|
4411
|
-
if (
|
|
4412
|
-
clearTimeout(
|
|
4413
|
-
|
|
5241
|
+
if (openTimer) {
|
|
5242
|
+
clearTimeout(openTimer)
|
|
5243
|
+
openTimer = null
|
|
5244
|
+
}
|
|
5245
|
+
if (closeTimer) {
|
|
5246
|
+
clearTimeout(closeTimer)
|
|
5247
|
+
closeTimer = null
|
|
4414
5248
|
}
|
|
4415
5249
|
},
|
|
5250
|
+
scheduleOpen: function () {
|
|
5251
|
+
if (openTimer) clearTimeout(openTimer)
|
|
5252
|
+
openTimer = setTimeout(function () {
|
|
5253
|
+
openTimer = null
|
|
5254
|
+
open()
|
|
5255
|
+
}, openDelay)
|
|
5256
|
+
},
|
|
4416
5257
|
scheduleClose: function () {
|
|
4417
|
-
if (
|
|
4418
|
-
|
|
5258
|
+
if (openTimer) {
|
|
5259
|
+
clearTimeout(openTimer)
|
|
5260
|
+
openTimer = null
|
|
5261
|
+
}
|
|
5262
|
+
if (closeTimer) clearTimeout(closeTimer)
|
|
5263
|
+
closeTimer = setTimeout(function () {
|
|
5264
|
+
closeTimer = null
|
|
5265
|
+
close()
|
|
5266
|
+
}, closeDelay)
|
|
4419
5267
|
},
|
|
4420
5268
|
}
|
|
4421
5269
|
}
|
|
@@ -4643,6 +5491,10 @@ window.__ModuleLoader__.load({
|
|
|
4643
5491
|
|
|
4644
5492
|
var permDocPointerListener = null
|
|
4645
5493
|
var permResizeListener = null
|
|
5494
|
+
/** The context meter this skin last moved out of the host's dock line. */
|
|
5495
|
+
var dockedMeter = null
|
|
5496
|
+
/** Identity of the stats bindings THIS generation installed (see bindStatsHover). */
|
|
5497
|
+
var statsBindingToken = {}
|
|
4646
5498
|
|
|
4647
5499
|
/** Every dismiss route (item pick, outside pointer, resize/scroll, Escape) closes the menu through this one path. */
|
|
4648
5500
|
function closePermMenu() {
|
|
@@ -4727,13 +5579,13 @@ window.__ModuleLoader__.load({
|
|
|
4727
5579
|
popover.setAttribute('data-open', 'true')
|
|
4728
5580
|
}
|
|
4729
5581
|
|
|
4730
|
-
permHoverIntent = createHoverIntent(openPerm, closePermMenu,
|
|
5582
|
+
permHoverIntent = createHoverIntent(openPerm, closePermMenu, POPOVER_OPEN_DELAY, POPOVER_CLOSE_DELAY)
|
|
4731
5583
|
|
|
4732
5584
|
btn.addEventListener('mouseenter', function () {
|
|
4733
|
-
if (readPrefs().autoPopover)
|
|
5585
|
+
if (readPrefs().autoPopover === AUTO_POPOVER_ALL) permHoverIntent.scheduleOpen()
|
|
4734
5586
|
})
|
|
4735
5587
|
btn.addEventListener('mouseleave', function () {
|
|
4736
|
-
if (readPrefs().autoPopover) permHoverIntent.scheduleClose()
|
|
5588
|
+
if (readPrefs().autoPopover === AUTO_POPOVER_ALL) permHoverIntent.scheduleClose()
|
|
4737
5589
|
})
|
|
4738
5590
|
popover.addEventListener('mouseenter', function () {
|
|
4739
5591
|
permHoverIntent.cancel()
|
|
@@ -4938,6 +5790,75 @@ window.__ModuleLoader__.load({
|
|
|
4938
5790
|
else row.appendChild(stats)
|
|
4939
5791
|
}
|
|
4940
5792
|
|
|
5793
|
+
/** The composer's dock line: the stack child that is not the card. The host
|
|
5794
|
+
* parks the stats pills there, and since 0.1.7 the context meter too. */
|
|
5795
|
+
function composerDock(card) {
|
|
5796
|
+
var stack = card.parentElement
|
|
5797
|
+
if (stack === null) return null
|
|
5798
|
+
for (var i = 0; i < stack.children.length; i++) {
|
|
5799
|
+
if (stack.children[i] !== card) return stack.children[i]
|
|
5800
|
+
}
|
|
5801
|
+
return null
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
/** The meter the host parks in that dock, or null when it is not there. */
|
|
5805
|
+
function dockedContextMeter(dock) {
|
|
5806
|
+
if (dock === null) return null
|
|
5807
|
+
var triggers = dock.querySelectorAll('button[aria-haspopup="dialog"]')
|
|
5808
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
5809
|
+
// The meter's trigger IS the occupancy reading ("42%") and no stats
|
|
5810
|
+
// pill ever is, so the label alone identifies it without a class name
|
|
5811
|
+
// (the host hashes those per build).
|
|
5812
|
+
if (!/^\d{1,3}%$/.test((triggers[i].textContent || '').trim())) continue
|
|
5813
|
+
var node = triggers[i]
|
|
5814
|
+
while (node.parentElement !== null && node.parentElement !== dock) node = node.parentElement
|
|
5815
|
+
return node
|
|
5816
|
+
}
|
|
5817
|
+
return null
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
/**
|
|
5821
|
+
* Merge the context-occupancy meter into the toolbar row beside the stats.
|
|
5822
|
+
* DSH 0.1.7 moved the meter out of the input bar's trailing cluster into
|
|
5823
|
+
* the dock line below the card, so a skin that merges only the stats
|
|
5824
|
+
* leaves it alone on a line of its own — a stray marker on a second row.
|
|
5825
|
+
* Older hosts keep it inside the card, where this finds nothing to move.
|
|
5826
|
+
* A host re-render can put it back, so the move is re-applied every pass.
|
|
5827
|
+
*/
|
|
5828
|
+
function mergeContextMeterIntoRow() {
|
|
5829
|
+
var card = document.querySelector('[data-composer-card]')
|
|
5830
|
+
if (card === null) return
|
|
5831
|
+
var dock = composerDock(card)
|
|
5832
|
+
var row = card.querySelector('[class*="_row"]')
|
|
5833
|
+
if (row === null) return
|
|
5834
|
+
if (!ui.copy.isComposerActive()) {
|
|
5835
|
+
if (dockedMeter !== null && dock !== null && row.contains(dockedMeter)) dock.appendChild(dockedMeter)
|
|
5836
|
+
return
|
|
5837
|
+
}
|
|
5838
|
+
var meter = dockedContextMeter(dock)
|
|
5839
|
+
if (meter === null) return
|
|
5840
|
+
dockedMeter = meter
|
|
5841
|
+
// The stamp is what the stylesheet hangs the shared composer-line type
|
|
5842
|
+
// on: the host's own class names are hashed per build, and the ring's
|
|
5843
|
+
// trigger has no other stable hook.
|
|
5844
|
+
meter.setAttribute('data-dsh-claude-context-meter', '')
|
|
5845
|
+
var trailing = row.querySelector('[class*="trailing"]')
|
|
5846
|
+
if (trailing === null) { row.appendChild(meter); return }
|
|
5847
|
+
// Right of the model trigger, which lives inside the trailing cluster:
|
|
5848
|
+
// park the ring after whichever trailing child owns that trigger (our
|
|
5849
|
+
// own button when the skin draws the seat, the host's cluster when the
|
|
5850
|
+
// picker preference hands it back).
|
|
5851
|
+
var anchor = trailing.querySelector('[class*="standardControls"]')
|
|
5852
|
+
var modelBtn = trailing.querySelector('.dsh-claude-model-btn')
|
|
5853
|
+
if (modelBtn !== null) {
|
|
5854
|
+
var node = modelBtn
|
|
5855
|
+
while (node.parentElement !== null && node.parentElement !== trailing) node = node.parentElement
|
|
5856
|
+
if (node.parentElement === trailing) anchor = node
|
|
5857
|
+
}
|
|
5858
|
+
if (anchor !== null) trailing.insertBefore(meter, anchor.nextSibling)
|
|
5859
|
+
else trailing.appendChild(meter)
|
|
5860
|
+
}
|
|
5861
|
+
|
|
4941
5862
|
// Re-insert when a re-render swapped the host row, then mirror the running preset.
|
|
4942
5863
|
function syncSegments() {
|
|
4943
5864
|
var isHero = ui.copy.isHeroView()
|
|
@@ -5188,12 +6109,30 @@ window.__ModuleLoader__.load({
|
|
|
5188
6109
|
if (statsPopover !== null) statsPopover.setAttribute('data-open', 'false')
|
|
5189
6110
|
}
|
|
5190
6111
|
|
|
6112
|
+
/**
|
|
6113
|
+
* Drop stats cards left behind by a previous client generation.
|
|
6114
|
+
*
|
|
6115
|
+
* Client HMR drops the old fiber's disposals, so the teardown never runs:
|
|
6116
|
+
* the previous generation's card stays in <body> holding its last content
|
|
6117
|
+
* and `data-open="true"` — a second, frozen popover sitting beside the
|
|
6118
|
+
* live one (which is why the two read differently: the stale card shows
|
|
6119
|
+
* whatever sections it was last rendered with). The model picker and the
|
|
6120
|
+
* account footer sweep their own strays the same way; the stats card was
|
|
6121
|
+
* the one that did not.
|
|
6122
|
+
*/
|
|
6123
|
+
function sweepStrayStatsPopovers() {
|
|
6124
|
+
var strays = document.querySelectorAll('body > .dsh-claude-stats-popover')
|
|
6125
|
+
for (var i = 0; i < strays.length; i++) {
|
|
6126
|
+
if (strays[i] !== statsPopover) strays[i].parentElement.removeChild(strays[i])
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
|
|
5191
6130
|
function scheduleHideStatsPopover() {
|
|
5192
6131
|
if (statsHideTimer) clearTimeout(statsHideTimer)
|
|
5193
6132
|
statsHideTimer = setTimeout(function () {
|
|
5194
6133
|
statsHideTimer = null
|
|
5195
6134
|
hideStatsPopover()
|
|
5196
|
-
},
|
|
6135
|
+
}, POPOVER_CLOSE_DELAY)
|
|
5197
6136
|
}
|
|
5198
6137
|
|
|
5199
6138
|
function renderStatsPopover(sections) {
|
|
@@ -5238,18 +6177,55 @@ window.__ModuleLoader__.load({
|
|
|
5238
6177
|
}
|
|
5239
6178
|
|
|
5240
6179
|
function bindStatsHover(root) {
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
//
|
|
5244
|
-
//
|
|
5245
|
-
//
|
|
5246
|
-
|
|
5247
|
-
|
|
6180
|
+
// Generation-scoped, not a plain boolean: the host owns the stats node
|
|
6181
|
+
// and reuses it across a client HMR reload, so a boolean left by the
|
|
6182
|
+
// previous generation made this one skip binding entirely — the OLD
|
|
6183
|
+
// closure kept serving the card (its content, its stale node) while this
|
|
6184
|
+
// generation's listeners never existed.
|
|
6185
|
+
if (root.__dshStatsHoverToken === statsBindingToken) return
|
|
6186
|
+
root.__dshStatsHoverToken = statsBindingToken
|
|
6187
|
+
var openTimer = null
|
|
6188
|
+
function cancelOpen() {
|
|
6189
|
+
if (openTimer) {
|
|
6190
|
+
clearTimeout(openTimer)
|
|
6191
|
+
openTimer = null
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
// Hover still opens the card under "All" — the setting promises exactly
|
|
6195
|
+
// that — but only after a deliberate dwell: the stats sentence sits in
|
|
6196
|
+
// the MIDDLE of the composer row, so a pointer on its way from the
|
|
6197
|
+
// permission selector to the model trigger used to unfold the card on
|
|
6198
|
+
// the way past. A passing pointer never stays the dwell out; a pointer
|
|
6199
|
+
// the user actually parked there does.
|
|
5248
6200
|
root.addEventListener('mouseenter', function () {
|
|
5249
|
-
|
|
6201
|
+
cancelOpen()
|
|
6202
|
+
if (readPrefs().autoPopover !== AUTO_POPOVER_ALL) return
|
|
6203
|
+
openTimer = setTimeout(function () {
|
|
6204
|
+
openTimer = null
|
|
6205
|
+
showStatsPopover(root)
|
|
6206
|
+
}, 300)
|
|
5250
6207
|
})
|
|
5251
6208
|
root.addEventListener('mouseleave', function () {
|
|
5252
|
-
|
|
6209
|
+
cancelOpen()
|
|
6210
|
+
// Unconditional, unlike the open side: a card opened by CLICK has to
|
|
6211
|
+
// close when the pointer leaves, whatever the hover switch says.
|
|
6212
|
+
scheduleHideStatsPopover()
|
|
6213
|
+
})
|
|
6214
|
+
// The host's own stats dialogs are hidden by the stylesheet, so a click
|
|
6215
|
+
// has to land somewhere: it opens this card — the only stats surface
|
|
6216
|
+
// left when the hover switch is off or scoped to the account rail.
|
|
6217
|
+
//
|
|
6218
|
+
// Only a REAL click counts. Collecting the card's content means clicking
|
|
6219
|
+
// the host's two pills to open their dialogs, and those synthetic clicks
|
|
6220
|
+
// bubble back up to this listener: re-entering the collection from
|
|
6221
|
+
// inside itself made two reads race, which is why sections went missing
|
|
6222
|
+
// (the card kept flipping between "会话统计 + Token 用量" and "Token 用量"
|
|
6223
|
+
// alone) and why the card stopped closing — every re-entry cancelled the
|
|
6224
|
+
// pending hide.
|
|
6225
|
+
root.addEventListener('click', function (event) {
|
|
6226
|
+
if (event && event.isTrusted === false) return
|
|
6227
|
+
cancelOpen()
|
|
6228
|
+
showStatsPopover(root)
|
|
5253
6229
|
})
|
|
5254
6230
|
}
|
|
5255
6231
|
|
|
@@ -5292,11 +6268,14 @@ window.__ModuleLoader__.load({
|
|
|
5292
6268
|
sync: function () {
|
|
5293
6269
|
syncAttachmentState()
|
|
5294
6270
|
mergeStatsIntoRow()
|
|
6271
|
+
mergeContextMeterIntoRow()
|
|
6272
|
+
sweepStrayStatsPopovers()
|
|
5295
6273
|
syncStatsSummary()
|
|
5296
6274
|
syncSegments()
|
|
5297
6275
|
syncChatTabComposer()
|
|
5298
6276
|
},
|
|
5299
|
-
closeMenu: closePermMenu
|
|
6277
|
+
closeMenu: closePermMenu,
|
|
6278
|
+
closeStats: hideStatsPopover
|
|
5300
6279
|
}
|
|
5301
6280
|
|
|
5302
6281
|
return function () {
|
|
@@ -5326,15 +6305,6 @@ window.__ModuleLoader__.load({
|
|
|
5326
6305
|
}
|
|
5327
6306
|
}
|
|
5328
6307
|
|
|
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
6308
|
function modelEl(tag, cls, text) {
|
|
5339
6309
|
var el = document.createElement(tag)
|
|
5340
6310
|
if (cls) el.className = cls
|
|
@@ -5353,11 +6323,10 @@ window.__ModuleLoader__.load({
|
|
|
5353
6323
|
* model it did not make (an OpenCode row wearing OpenCode's mark beside
|
|
5354
6324
|
* "LongCat-2.0"), which reads as a wrong answer rather than a missing one.
|
|
5355
6325
|
*
|
|
5356
|
-
* @param groupId - provider route id (unused; kept for call-site symmetry).
|
|
5357
6326
|
* @param modelId - catalog model id.
|
|
5358
6327
|
* @returns the vendored lockup's id, or null when no rule claims it.
|
|
5359
6328
|
*/
|
|
5360
|
-
function modelBrand(
|
|
6329
|
+
function modelBrand(modelId) {
|
|
5361
6330
|
if (modelCopy !== null) {
|
|
5362
6331
|
var id = String(modelId === void 0 || modelId === null ? '' : modelId).toLowerCase()
|
|
5363
6332
|
for (var i = 0; i < modelCopy.brandRules.length; i++) {
|
|
@@ -5419,6 +6388,418 @@ window.__ModuleLoader__.load({
|
|
|
5419
6388
|
return el
|
|
5420
6389
|
}
|
|
5421
6390
|
|
|
6391
|
+
/**
|
|
6392
|
+
* Resolving a catalog model to its description line.
|
|
6393
|
+
*
|
|
6394
|
+
* The copy document (src/model-descriptions.json) ships as data beside the
|
|
6395
|
+
* bundle and is fetched at runtime, so this is the only place that knows how
|
|
6396
|
+
* a model's id becomes a sentence. Resolution descends: exact entry (one
|
|
6397
|
+
* model resold by several providers folds to a single key) → family rule →
|
|
6398
|
+
* tier rule → the catalog's own text. Family rules are ordered and anchored
|
|
6399
|
+
* so another vendor's flash tier never borrows DeepSeek's copy; the tier
|
|
6400
|
+
* rules are the last resort, read out of the id itself. A model this table
|
|
6401
|
+
* has never seen and the catalog does not describe resolves to an empty
|
|
6402
|
+
* string on purpose: a name-only row beats an invented line.
|
|
6403
|
+
*
|
|
6404
|
+
* Split out of model-picker.js when the effort slider pushed that fragment
|
|
6405
|
+
* past the repository's size stop line; nothing here touches the picker's
|
|
6406
|
+
* closure, only the shared copy document.
|
|
6407
|
+
*/
|
|
6408
|
+
/** Exact entry: `provider/model`, bare id, folded id, then the alias table. */
|
|
6409
|
+
function exactModelCopy(groupId, modelId) {
|
|
6410
|
+
if (modelCopy === null) return null
|
|
6411
|
+
var gid = String(groupId === void 0 || groupId === null ? '' : groupId).toLowerCase()
|
|
6412
|
+
var mid = String(modelId === void 0 || modelId === null ? '' : modelId)
|
|
6413
|
+
var midLower = mid.toLowerCase()
|
|
6414
|
+
var byProvider = modelCopy.exact[groupId + '/' + mid] || modelCopy.exact[gid + '/' + midLower]
|
|
6415
|
+
if (byProvider) return byProvider
|
|
6416
|
+
if (modelCopy.exact[mid]) return modelCopy.exact[mid]
|
|
6417
|
+
if (modelCopy.exact[midLower]) return modelCopy.exact[midLower]
|
|
6418
|
+
var folded = normalizeModelId(mid)
|
|
6419
|
+
if (modelCopy.folded[folded]) return modelCopy.folded[folded]
|
|
6420
|
+
var alias = modelCopy.aliases[mid] || modelCopy.aliases[midLower] || modelCopy.aliases[folded] || (modelCopy.foldedAliases && modelCopy.foldedAliases[folded])
|
|
6421
|
+
if (alias) {
|
|
6422
|
+
if (modelCopy.exact[alias]) return modelCopy.exact[alias]
|
|
6423
|
+
var foldedAlias = normalizeModelId(alias)
|
|
6424
|
+
if (modelCopy.folded[foldedAlias]) return modelCopy.folded[foldedAlias]
|
|
6425
|
+
}
|
|
6426
|
+
return null
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
/**
|
|
6430
|
+
* Family entry. The model id is tried alone first because it is the stronger
|
|
6431
|
+
* signal, then `provider/id` for ids that carry no brand of their own
|
|
6432
|
+
* (`abab6.5s-chat` under a provider called `minimax`).
|
|
6433
|
+
*/
|
|
6434
|
+
function familyModelCopy(groupId, modelId) {
|
|
6435
|
+
if (modelCopy === null) return null
|
|
6436
|
+
var id = String(modelId === void 0 || modelId === null ? '' : modelId).toLowerCase()
|
|
6437
|
+
var haystacks = [id, String(groupId === void 0 || groupId === null ? '' : groupId).toLowerCase() + '/' + id]
|
|
6438
|
+
for (var h = 0; h < haystacks.length; h++) {
|
|
6439
|
+
for (var i = 0; i < modelCopy.families.length; i++) {
|
|
6440
|
+
var rule = modelCopy.families[i]
|
|
6441
|
+
if (!rule.re.test(haystacks[h])) continue
|
|
6442
|
+
if (rule.key) return modelCopy.exact[rule.key] || null
|
|
6443
|
+
return rule.text
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
return null
|
|
6447
|
+
}
|
|
6448
|
+
|
|
6449
|
+
/** Last-resort tier rule, read out of the id itself. */
|
|
6450
|
+
function tierModelCopy(modelId) {
|
|
6451
|
+
if (modelCopy === null) return null
|
|
6452
|
+
var id = String(modelId === void 0 || modelId === null ? '' : modelId).toLowerCase()
|
|
6453
|
+
for (var i = 0; i < modelCopy.tiers.length; i++) {
|
|
6454
|
+
if (modelCopy.tiers[i].re.test(id)) return modelCopy.tiers[i].text
|
|
6455
|
+
}
|
|
6456
|
+
return null
|
|
6457
|
+
}
|
|
6458
|
+
|
|
6459
|
+
/**
|
|
6460
|
+
* The description line for one catalog model, in the shell's language.
|
|
6461
|
+
* `ctx` is the caller's context, used only to read the shell's locale.
|
|
6462
|
+
*/
|
|
6463
|
+
function modelDescription(ctx, groupId, model) {
|
|
6464
|
+
var id = typeof model.id === 'string' ? model.id : ''
|
|
6465
|
+
var pair = exactModelCopy(groupId, id) || familyModelCopy(groupId, id) || tierModelCopy(id)
|
|
6466
|
+
var text = localized(pair, ctx)
|
|
6467
|
+
if (text) return text
|
|
6468
|
+
return typeof model.description === 'string' ? model.description : ''
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
/**
|
|
6472
|
+
* The reasoning-effort slider that sits at the foot of the model picker's
|
|
6473
|
+
* first level.
|
|
6474
|
+
*
|
|
6475
|
+
* The catalog describes a model's reasoning as a discrete ladder
|
|
6476
|
+
* (`reasoning.efforts` plus the level in force), and this control draws that
|
|
6477
|
+
* ladder as a track: the label carries the level's name, `Faster` / `Smarter`
|
|
6478
|
+
* name the direction of travel, and the knob follows the pointer
|
|
6479
|
+
* CONTINUOUSLY rather than hopping stop to stop. The value settles only when
|
|
6480
|
+
* the gesture ends — release, or the pointer leaving the control — and it
|
|
6481
|
+
* settles on the NEAREST level, so dragging never reads as a hard switch
|
|
6482
|
+
* between fixed positions.
|
|
6483
|
+
*
|
|
6484
|
+
* A model that offers no levels still gets the slider, because the picker
|
|
6485
|
+
* always wears the same shape at its foot: the knob travels the same way and
|
|
6486
|
+
* eases back to its resting end, but there is nothing to snap to and nothing
|
|
6487
|
+
* to select.
|
|
6488
|
+
*
|
|
6489
|
+
* The element is built once and reused across the picker's re-renders: a
|
|
6490
|
+
* rebuild mid-gesture would detach it and drop the pointer capture, so the
|
|
6491
|
+
* picker skips its render pass while a drag is in flight (see
|
|
6492
|
+
* src/overrides/model-picker.js).
|
|
6493
|
+
*
|
|
6494
|
+
* @param opts - `{ read, onPick, onDragStart }`. `read()` returns the model
|
|
6495
|
+
* picker's `modelEffort()` result for the current seat (or null), and
|
|
6496
|
+
* `onPick(levelId)` commits one level — `undefined` is the model's own
|
|
6497
|
+
* default level.
|
|
6498
|
+
* @returns `{ el, update, isDragging }`.
|
|
6499
|
+
*/
|
|
6500
|
+
function createEffortControl(opts) {
|
|
6501
|
+
var root = modelEl('div', 'dsh-claude-effort')
|
|
6502
|
+
var head = modelEl('div', 'dsh-claude-effort-head')
|
|
6503
|
+
var labelEl = modelEl('span', 'dsh-claude-effort-label')
|
|
6504
|
+
var valueEl = modelEl('span', 'dsh-claude-effort-value')
|
|
6505
|
+
var ends = modelEl('div', 'dsh-claude-effort-ends')
|
|
6506
|
+
var fasterEl = modelEl('span', 'dsh-claude-effort-end')
|
|
6507
|
+
var smarterEl = modelEl('span', 'dsh-claude-effort-end')
|
|
6508
|
+
var track = modelEl('div', 'dsh-claude-effort-track')
|
|
6509
|
+
var knob = modelEl('div', 'dsh-claude-effort-knob')
|
|
6510
|
+
|
|
6511
|
+
head.appendChild(labelEl)
|
|
6512
|
+
head.appendChild(valueEl)
|
|
6513
|
+
ends.appendChild(fasterEl)
|
|
6514
|
+
ends.appendChild(smarterEl)
|
|
6515
|
+
track.appendChild(knob)
|
|
6516
|
+
root.appendChild(head)
|
|
6517
|
+
root.appendChild(ends)
|
|
6518
|
+
root.appendChild(track)
|
|
6519
|
+
track.setAttribute('role', 'slider')
|
|
6520
|
+
track.setAttribute('tabindex', '0')
|
|
6521
|
+
|
|
6522
|
+
/** The ladder in force, the level it is on, and the words around it. */
|
|
6523
|
+
var steps = []
|
|
6524
|
+
var selected = -1
|
|
6525
|
+
var live = -1
|
|
6526
|
+
var dragging = false
|
|
6527
|
+
var painted = false
|
|
6528
|
+
var pointerId = null
|
|
6529
|
+
/** The dash shown when there is no level to name; cached, not re-read per move. */
|
|
6530
|
+
var noneLabel = ''
|
|
6531
|
+
|
|
6532
|
+
/**
|
|
6533
|
+
* The ladder as ordered steps, the step in force, and the words. A model
|
|
6534
|
+
* with no reasoning (or one whose catalog entry has not loaded) resolves to
|
|
6535
|
+
* an empty ladder: the slider is still shown, it just has nothing to
|
|
6536
|
+
* select. The catalog may leave out a default level (`defaultEffort` is
|
|
6537
|
+
* undefined) — that "Default" is a real position on the track, so it leads
|
|
6538
|
+
* the steps with an undefined id.
|
|
6539
|
+
*/
|
|
6540
|
+
function state() {
|
|
6541
|
+
var effort = opts.read()
|
|
6542
|
+
var list = []
|
|
6543
|
+
var index = -1
|
|
6544
|
+
if (effort !== null) {
|
|
6545
|
+
if (effort.reasoning.defaultEffort === void 0) list.push({ id: void 0, name: MODEL_EFFORT_DEFAULT })
|
|
6546
|
+
for (var i = 0; i < effort.reasoning.efforts.length; i++) {
|
|
6547
|
+
list.push({ id: effort.reasoning.efforts[i].id, name: effort.reasoning.efforts[i].name })
|
|
6548
|
+
}
|
|
6549
|
+
index = 0
|
|
6550
|
+
for (var s = 0; s < list.length; s++) {
|
|
6551
|
+
if (list[s].id === effort.effective) { index = s; break }
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
return {
|
|
6555
|
+
steps: list,
|
|
6556
|
+
index: index,
|
|
6557
|
+
labels: {
|
|
6558
|
+
label: copyLabel('effortLabel', MODEL_EFFORT_LABEL),
|
|
6559
|
+
faster: copyLabel('effortFaster', MODEL_EFFORT_FASTER),
|
|
6560
|
+
smarter: copyLabel('effortSmarter', MODEL_EFFORT_SMARTER),
|
|
6561
|
+
none: MODEL_EFFORT_NONE,
|
|
6562
|
+
},
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
/** How wide the knob is, and how far its centre may travel. */
|
|
6567
|
+
function geometry() {
|
|
6568
|
+
var size = knob.offsetWidth || 16
|
|
6569
|
+
return { size: size, span: Math.max(0, track.clientWidth - size) }
|
|
6570
|
+
}
|
|
6571
|
+
|
|
6572
|
+
/** The knob's x for one level. No levels (or a single one) rest at the end. */
|
|
6573
|
+
function positionFor(index) {
|
|
6574
|
+
var box = geometry()
|
|
6575
|
+
if (index < 0 || steps.length < 2) return box.span
|
|
6576
|
+
return (index / (steps.length - 1)) * box.span
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
/**
|
|
6580
|
+
* Move the knob. While the gesture runs this is ONE compositor write:
|
|
6581
|
+
* the stylesheet already forces `transition: none` for `data-dragging`,
|
|
6582
|
+
* so an inline transition dance (and the forced reflow it needs to take
|
|
6583
|
+
* effect) would be pure overhead on every pointermove. The inline dance
|
|
6584
|
+
* survives only for the rare non-drag instant move (a ladder swap on
|
|
6585
|
+
* first paint), where the stylesheet's transition would otherwise
|
|
6586
|
+
* animate the jump.
|
|
6587
|
+
*/
|
|
6588
|
+
function place(x, animate) {
|
|
6589
|
+
var value = 'translateX(' + Math.round(x) + 'px)'
|
|
6590
|
+
if (animate || dragging) {
|
|
6591
|
+
knob.style.transform = value
|
|
6592
|
+
return
|
|
6593
|
+
}
|
|
6594
|
+
knob.style.transition = 'none'
|
|
6595
|
+
knob.style.transform = value
|
|
6596
|
+
void knob.offsetWidth
|
|
6597
|
+
knob.style.transition = ''
|
|
6598
|
+
}
|
|
6599
|
+
|
|
6600
|
+
/** Where the pointer sits along the track, in knob-travel units. */
|
|
6601
|
+
function pointerTravel(clientX) {
|
|
6602
|
+
var box = geometry()
|
|
6603
|
+
var raw = clientX - track.getBoundingClientRect().left - box.size / 2
|
|
6604
|
+
return Math.max(0, Math.min(box.span, raw))
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
/** The level nearest a travel position: the one a release settles on. */
|
|
6608
|
+
function nearest(x) {
|
|
6609
|
+
if (steps.length === 0) return -1
|
|
6610
|
+
if (steps.length === 1) return 0
|
|
6611
|
+
var box = geometry()
|
|
6612
|
+
var ratio = box.span === 0 ? 0 : x / box.span
|
|
6613
|
+
return Math.max(0, Math.min(steps.length - 1, Math.round(ratio * (steps.length - 1))))
|
|
6614
|
+
}
|
|
6615
|
+
|
|
6616
|
+
/**
|
|
6617
|
+
* The level's name while the gesture runs, the committed one otherwise.
|
|
6618
|
+
* Same-value guard: the write runs per frame while dragging, and an
|
|
6619
|
+
* identical textContent assignment still replaces the text node — the
|
|
6620
|
+
* resulting mutation would feed the scheduler's observer and keep a full
|
|
6621
|
+
* pass running every frame.
|
|
6622
|
+
*/
|
|
6623
|
+
function paintValue() {
|
|
6624
|
+
var at = dragging ? live : selected
|
|
6625
|
+
var text = at >= 0 && steps[at] ? steps[at].name : noneLabel
|
|
6626
|
+
if (valueEl.textContent !== text) valueEl.textContent = text
|
|
6627
|
+
}
|
|
6628
|
+
|
|
6629
|
+
function paintAria(labels) {
|
|
6630
|
+
noneLabel = labels.none
|
|
6631
|
+
// Same-value guards throughout: update() runs on every scheduler pass,
|
|
6632
|
+
// and an identical write here (in particular the aria-label, which is
|
|
6633
|
+
// in the observer's attributeFilter) re-schedules the next pass — a
|
|
6634
|
+
// self-sustaining one-pass-per-frame loop.
|
|
6635
|
+
if (labelEl.textContent !== labels.label) labelEl.textContent = labels.label
|
|
6636
|
+
if (fasterEl.textContent !== labels.faster) fasterEl.textContent = labels.faster
|
|
6637
|
+
if (smarterEl.textContent !== labels.smarter) smarterEl.textContent = labels.smarter
|
|
6638
|
+
if (track.getAttribute('aria-label') !== labels.label) track.setAttribute('aria-label', labels.label)
|
|
6639
|
+
if (steps.length === 0) {
|
|
6640
|
+
root.setAttribute('data-empty', '')
|
|
6641
|
+
track.setAttribute('aria-disabled', 'true')
|
|
6642
|
+
track.removeAttribute('aria-valuemin')
|
|
6643
|
+
track.removeAttribute('aria-valuemax')
|
|
6644
|
+
track.removeAttribute('aria-valuenow')
|
|
6645
|
+
track.removeAttribute('aria-valuetext')
|
|
6646
|
+
return
|
|
6647
|
+
}
|
|
6648
|
+
var at = Math.max(0, selected)
|
|
6649
|
+
root.removeAttribute('data-empty')
|
|
6650
|
+
track.removeAttribute('aria-disabled')
|
|
6651
|
+
track.setAttribute('aria-valuemin', '0')
|
|
6652
|
+
track.setAttribute('aria-valuemax', String(steps.length - 1))
|
|
6653
|
+
track.setAttribute('aria-valuenow', String(at))
|
|
6654
|
+
track.setAttribute('aria-valuetext', steps[at] ? steps[at].name : '')
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
/** Commit one level, once: the host is told only when the level moved. */
|
|
6658
|
+
function commit(index) {
|
|
6659
|
+
if (index < 0 || index >= steps.length || index === selected) return
|
|
6660
|
+
selected = index
|
|
6661
|
+
opts.onPick(steps[index].id)
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
function releaseCapture() {
|
|
6665
|
+
try {
|
|
6666
|
+
if (pointerId !== null && track.hasPointerCapture && track.hasPointerCapture(pointerId)) {
|
|
6667
|
+
track.releasePointerCapture(pointerId)
|
|
6668
|
+
}
|
|
6669
|
+
} catch (error) { /* the capture may already be gone */ }
|
|
6670
|
+
pointerId = null
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
/** The gesture ends: the knob lands on the nearest level and commits it. */
|
|
6674
|
+
function settle() {
|
|
6675
|
+
if (!dragging) return
|
|
6676
|
+
if (moveQueued) {
|
|
6677
|
+
// Land the still-pending position first, so the release settles on
|
|
6678
|
+
// where the pointer actually is rather than one event behind.
|
|
6679
|
+
cancelAnimationFrame(pendingFrame)
|
|
6680
|
+
moveQueued = false
|
|
6681
|
+
pendingFrame = 0
|
|
6682
|
+
applyPending()
|
|
6683
|
+
if (!dragging) return
|
|
6684
|
+
}
|
|
6685
|
+
dragging = false
|
|
6686
|
+
root.removeAttribute('data-dragging')
|
|
6687
|
+
releaseCapture()
|
|
6688
|
+
var index = live
|
|
6689
|
+
live = -1
|
|
6690
|
+
place(positionFor(index), true)
|
|
6691
|
+
paintValue()
|
|
6692
|
+
commit(index)
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
function onPointerDown(e) {
|
|
6696
|
+
if (e.button !== 0) return
|
|
6697
|
+
e.preventDefault()
|
|
6698
|
+
e.stopPropagation()
|
|
6699
|
+
dragging = true
|
|
6700
|
+
painted = true
|
|
6701
|
+
root.setAttribute('data-dragging', '')
|
|
6702
|
+
pointerId = e.pointerId
|
|
6703
|
+
try { track.setPointerCapture(e.pointerId) } catch (error) { /* capture is a nicety */ }
|
|
6704
|
+
if (typeof opts.onDragStart === 'function') opts.onDragStart()
|
|
6705
|
+
var x = pointerTravel(e.clientX)
|
|
6706
|
+
place(x, false)
|
|
6707
|
+
live = nearest(x)
|
|
6708
|
+
paintValue()
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
/**
|
|
6712
|
+
* The gesture's latest pointer position. pointermove fires faster than
|
|
6713
|
+
* frames render; the event stores the position and ONE animation-frame
|
|
6714
|
+
* callback applies it — reads first (boundary box, travel geometry),
|
|
6715
|
+
* writes after (one transform + the level name) — so the drag costs one
|
|
6716
|
+
* layout flush per frame instead of one forced reflow per event.
|
|
6717
|
+
*/
|
|
6718
|
+
var pendingFrame = 0
|
|
6719
|
+
var moveQueued = false
|
|
6720
|
+
var pendingX = 0
|
|
6721
|
+
var pendingY = 0
|
|
6722
|
+
|
|
6723
|
+
function applyPending() {
|
|
6724
|
+
if (!dragging) return
|
|
6725
|
+
var box = root.getBoundingClientRect()
|
|
6726
|
+
// Leaving the control ends the gesture where it stands — the knob
|
|
6727
|
+
// settles on the nearest level instead of trailing the pointer away.
|
|
6728
|
+
if (pendingX < box.left - 6 || pendingX > box.right + 6 || pendingY < box.top - 6 || pendingY > box.bottom + 6) {
|
|
6729
|
+
settle()
|
|
6730
|
+
return
|
|
6731
|
+
}
|
|
6732
|
+
var x = pointerTravel(pendingX)
|
|
6733
|
+
place(x, false)
|
|
6734
|
+
live = nearest(x)
|
|
6735
|
+
paintValue()
|
|
6736
|
+
}
|
|
6737
|
+
|
|
6738
|
+
function onPointerMove(e) {
|
|
6739
|
+
if (!dragging) return
|
|
6740
|
+
pendingX = e.clientX
|
|
6741
|
+
pendingY = e.clientY
|
|
6742
|
+
if (moveQueued) return
|
|
6743
|
+
moveQueued = true
|
|
6744
|
+
pendingFrame = requestAnimationFrame(function () {
|
|
6745
|
+
moveQueued = false
|
|
6746
|
+
pendingFrame = 0
|
|
6747
|
+
applyPending()
|
|
6748
|
+
})
|
|
6749
|
+
}
|
|
6750
|
+
|
|
6751
|
+
track.addEventListener('pointerdown', onPointerDown)
|
|
6752
|
+
track.addEventListener('pointermove', onPointerMove)
|
|
6753
|
+
track.addEventListener('pointerup', function () { settle() })
|
|
6754
|
+
track.addEventListener('pointercancel', function () { settle() })
|
|
6755
|
+
track.addEventListener('keydown', function (e) {
|
|
6756
|
+
if (steps.length === 0) return
|
|
6757
|
+
var base = selected < 0 ? 0 : selected
|
|
6758
|
+
var next = base
|
|
6759
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = Math.max(0, base - 1)
|
|
6760
|
+
else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = Math.min(steps.length - 1, base + 1)
|
|
6761
|
+
else if (e.key === 'Home') next = 0
|
|
6762
|
+
else if (e.key === 'End') next = steps.length - 1
|
|
6763
|
+
else return
|
|
6764
|
+
e.preventDefault()
|
|
6765
|
+
e.stopPropagation()
|
|
6766
|
+
commit(next)
|
|
6767
|
+
place(positionFor(next), true)
|
|
6768
|
+
paintAria(state().labels)
|
|
6769
|
+
paintValue()
|
|
6770
|
+
})
|
|
6771
|
+
|
|
6772
|
+
/**
|
|
6773
|
+
* Re-point the control at the model in force. The ladder is rebuilt only
|
|
6774
|
+
* when the levels themselves change; the knob is only animated when the
|
|
6775
|
+
* selected level moved on its own (the host echoing a selection back).
|
|
6776
|
+
*/
|
|
6777
|
+
function update() {
|
|
6778
|
+
var next = state()
|
|
6779
|
+
var changed = next.steps.length !== steps.length
|
|
6780
|
+
if (!changed) {
|
|
6781
|
+
for (var i = 0; i < next.steps.length; i++) {
|
|
6782
|
+
if (next.steps[i].id !== steps[i].id) { changed = true; break }
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
if (changed) steps = next.steps.slice()
|
|
6786
|
+
if (!dragging) {
|
|
6787
|
+
var moved = painted && !changed && selected !== next.index
|
|
6788
|
+
selected = next.index
|
|
6789
|
+
place(positionFor(selected), moved)
|
|
6790
|
+
painted = true
|
|
6791
|
+
}
|
|
6792
|
+
paintAria(next.labels)
|
|
6793
|
+
paintValue()
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
return {
|
|
6797
|
+
el: root,
|
|
6798
|
+
update: update,
|
|
6799
|
+
isDragging: function () { return dragging },
|
|
6800
|
+
}
|
|
6801
|
+
}
|
|
6802
|
+
|
|
5422
6803
|
function installModelPicker(ctx, ui) {
|
|
5423
6804
|
/**
|
|
5424
6805
|
* The host's model seat is a click-triggered two-pane menu (Model /
|
|
@@ -5438,80 +6819,92 @@ window.__ModuleLoader__.load({
|
|
|
5438
6819
|
var modelPop = null
|
|
5439
6820
|
var modelSubPop = null
|
|
5440
6821
|
var modelBody = null
|
|
6822
|
+
var modelFooter = null
|
|
6823
|
+
var effortSlider = null
|
|
5441
6824
|
var modelSubBody = null
|
|
5442
|
-
|
|
6825
|
+
/**
|
|
6826
|
+
* The picker is two cards wide, and the level-2 card only cancels a pending
|
|
6827
|
+
* close once the pointer is ON it — so the grace has to cover the journey
|
|
6828
|
+
* from a level-1 row, across the gap, onto the sub card. At the shared 100ms
|
|
6829
|
+
* a slow traverse ran it out and both cards folded up mid-journey.
|
|
6830
|
+
*/
|
|
6831
|
+
var MODEL_CLOSE_DELAY = 150
|
|
6832
|
+
/** Pending fold of level 2 while the pointer is still crossing level 1. */
|
|
6833
|
+
var subFoldTimer = null
|
|
6834
|
+
var modelHoverIntent = createHoverIntent(openModelPopover, closeModelIfAway, POPOVER_OPEN_DELAY, MODEL_CLOSE_DELAY)
|
|
6835
|
+
/** The More-models cell drills in on the same dwell/grace as the trigger. */
|
|
6836
|
+
var modelSubHoverIntent = createHoverIntent(openModelSub, closeModelIfAway, POPOVER_OPEN_DELAY, MODEL_CLOSE_DELAY)
|
|
5443
6837
|
var modelDir = null
|
|
5444
6838
|
var modelSub = null
|
|
5445
6839
|
var modelSessionId = null
|
|
5446
6840
|
var modelWarmRequested = false
|
|
5447
|
-
var modelSubKind = null
|
|
5448
6841
|
var modelBodySig = ''
|
|
5449
6842
|
var modelSubSig = ''
|
|
6843
|
+
/** Settings-page listeners waiting on the provider list. */
|
|
6844
|
+
var providerListeners = []
|
|
5450
6845
|
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
if (
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
var midLower = mid.toLowerCase()
|
|
5457
|
-
var byProvider = modelCopy.exact[groupId + '/' + mid] || modelCopy.exact[gid + '/' + midLower]
|
|
5458
|
-
if (byProvider) return byProvider
|
|
5459
|
-
if (modelCopy.exact[mid]) return modelCopy.exact[mid]
|
|
5460
|
-
if (modelCopy.exact[midLower]) return modelCopy.exact[midLower]
|
|
5461
|
-
var folded = normalizeModelId(mid)
|
|
5462
|
-
if (modelCopy.folded[folded]) return modelCopy.folded[folded]
|
|
5463
|
-
var alias = modelCopy.aliases[mid] || modelCopy.aliases[midLower] || modelCopy.aliases[folded] || (modelCopy.foldedAliases && modelCopy.foldedAliases[folded])
|
|
5464
|
-
if (alias) {
|
|
5465
|
-
if (modelCopy.exact[alias]) return modelCopy.exact[alias]
|
|
5466
|
-
var foldedAlias = normalizeModelId(alias)
|
|
5467
|
-
if (modelCopy.folded[foldedAlias]) return modelCopy.folded[foldedAlias]
|
|
6846
|
+
function cancelCloseModel() {
|
|
6847
|
+
modelHoverIntent.cancel()
|
|
6848
|
+
if (subFoldTimer !== null) {
|
|
6849
|
+
clearTimeout(subFoldTimer)
|
|
6850
|
+
subFoldTimer = null
|
|
5468
6851
|
}
|
|
5469
|
-
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
function scheduleCloseModel() {
|
|
6855
|
+
modelHoverIntent.scheduleClose()
|
|
5470
6856
|
}
|
|
5471
6857
|
|
|
5472
6858
|
/**
|
|
5473
|
-
*
|
|
5474
|
-
*
|
|
5475
|
-
*
|
|
6859
|
+
* Where the pointer last was, tracked only while a card is up. The two cards
|
|
6860
|
+
* are separate boxes with a sliver of desktop between them, and a pointer
|
|
6861
|
+
* crossing that sliver has not left the picker — without this the grace
|
|
6862
|
+
* simply runs out mid-gap and both cards fold.
|
|
5476
6863
|
*/
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
6864
|
+
var pointer = null
|
|
6865
|
+
var pointerBound = false
|
|
6866
|
+
function trackPointer(event) {
|
|
6867
|
+
pointer = { x: event.clientX, y: event.clientY }
|
|
6868
|
+
}
|
|
6869
|
+
function trackPointerWhileOpen() {
|
|
6870
|
+
var wanted = modelPop !== null && modelPop.getAttribute('data-open') === 'true'
|
|
6871
|
+
if (wanted === pointerBound) return
|
|
6872
|
+
pointerBound = wanted
|
|
6873
|
+
if (wanted) {
|
|
6874
|
+
document.addEventListener('mousemove', trackPointer, true)
|
|
6875
|
+
} else {
|
|
6876
|
+
document.removeEventListener('mousemove', trackPointer, true)
|
|
6877
|
+
pointer = null
|
|
5488
6878
|
}
|
|
5489
|
-
return null
|
|
5490
6879
|
}
|
|
5491
6880
|
|
|
5492
|
-
/**
|
|
5493
|
-
function
|
|
5494
|
-
if (
|
|
5495
|
-
var
|
|
5496
|
-
for (var i = 0; i <
|
|
5497
|
-
|
|
6881
|
+
/** True while the pointer sits in either card, or in the gap between them. */
|
|
6882
|
+
function pointerInPicker() {
|
|
6883
|
+
if (pointer === null) return false
|
|
6884
|
+
var cards = [modelPop, modelSubPop]
|
|
6885
|
+
for (var i = 0; i < cards.length; i++) {
|
|
6886
|
+
var card = cards[i]
|
|
6887
|
+
if (card === null || card.getAttribute('data-open') !== 'true') continue
|
|
6888
|
+
var box = card.getBoundingClientRect()
|
|
6889
|
+
if (pointer.x >= box.left - 8 && pointer.x <= box.right + 8 &&
|
|
6890
|
+
pointer.y >= box.top - 8 && pointer.y <= box.bottom + 8) return true
|
|
5498
6891
|
}
|
|
5499
|
-
return
|
|
5500
|
-
}
|
|
5501
|
-
|
|
5502
|
-
function cancelCloseModel() {
|
|
5503
|
-
modelHoverIntent.cancel()
|
|
6892
|
+
return false
|
|
5504
6893
|
}
|
|
5505
6894
|
|
|
5506
|
-
|
|
5507
|
-
|
|
6895
|
+
/** Close on leave, but treat the gap between the two cards as still inside. */
|
|
6896
|
+
function closeModelIfAway() {
|
|
6897
|
+
if (pointerInPicker()) {
|
|
6898
|
+
scheduleCloseModel()
|
|
6899
|
+
return
|
|
6900
|
+
}
|
|
6901
|
+
closeModelPopovers()
|
|
5508
6902
|
}
|
|
5509
6903
|
|
|
5510
6904
|
function closeModelPopovers() {
|
|
5511
6905
|
cancelCloseModel()
|
|
5512
6906
|
if (modelPop) modelPop.setAttribute('data-open', 'false')
|
|
5513
6907
|
if (modelSubPop) modelSubPop.setAttribute('data-open', 'false')
|
|
5514
|
-
modelSubKind = null
|
|
5515
6908
|
}
|
|
5516
6909
|
|
|
5517
6910
|
function openModelPopover() {
|
|
@@ -5527,16 +6920,14 @@ window.__ModuleLoader__.load({
|
|
|
5527
6920
|
}
|
|
5528
6921
|
} catch (error) { /* synchronous failure — the store's error surface covers it */ }
|
|
5529
6922
|
}
|
|
5530
|
-
modelSubKind = null
|
|
5531
6923
|
if (modelSubPop) modelSubPop.setAttribute('data-open', 'false')
|
|
5532
6924
|
renderModelBody()
|
|
5533
6925
|
positionModelPopovers()
|
|
5534
6926
|
if (modelPop) modelPop.setAttribute('data-open', 'true')
|
|
5535
6927
|
}
|
|
5536
6928
|
|
|
5537
|
-
function openModelSub(
|
|
6929
|
+
function openModelSub() {
|
|
5538
6930
|
cancelCloseModel()
|
|
5539
|
-
modelSubKind = kind
|
|
5540
6931
|
renderModelSub()
|
|
5541
6932
|
positionModelPopovers()
|
|
5542
6933
|
if (modelSubPop) modelSubPop.setAttribute('data-open', 'true')
|
|
@@ -5597,7 +6988,7 @@ window.__ModuleLoader__.load({
|
|
|
5597
6988
|
var store = modelDir.store
|
|
5598
6989
|
if (store && typeof store.subscribe === 'function') {
|
|
5599
6990
|
try {
|
|
5600
|
-
modelSub = store.subscribe(function () { if (ui.schedule) ui.schedule() })
|
|
6991
|
+
modelSub = store.subscribe(function () { notifyProviders(); if (ui.schedule) ui.schedule() })
|
|
5601
6992
|
} catch (error) {
|
|
5602
6993
|
modelSub = null
|
|
5603
6994
|
}
|
|
@@ -5611,6 +7002,37 @@ window.__ModuleLoader__.load({
|
|
|
5611
7002
|
try { return modelDir.store.getSnapshot() } catch (error) { return null }
|
|
5612
7003
|
}
|
|
5613
7004
|
|
|
7005
|
+
/**
|
|
7006
|
+
* The catalog's providers, in catalog order, each with its model count.
|
|
7007
|
+
*
|
|
7008
|
+
* The settings page's quick-provider picker is the other consumer, and it
|
|
7009
|
+
* may be opened before the picker itself ever was — so this resolves the
|
|
7010
|
+
* directory and starts the shared catalog load rather than requiring a
|
|
7011
|
+
* first popover open.
|
|
7012
|
+
*/
|
|
7013
|
+
function modelProviders() {
|
|
7014
|
+
modelDirectory()
|
|
7015
|
+
warmModelCatalog()
|
|
7016
|
+
var snap = modelSnapshot()
|
|
7017
|
+
var groups = (snap && snap.groups) || []
|
|
7018
|
+
var out = []
|
|
7019
|
+
for (var i = 0; i < groups.length; i++) {
|
|
7020
|
+
if (groups[i].models.length === 0) continue
|
|
7021
|
+
out.push({ id: groups[i].id, name: groups[i].name || groups[i].id, count: groups[i].models.length })
|
|
7022
|
+
}
|
|
7023
|
+
return out
|
|
7024
|
+
}
|
|
7025
|
+
|
|
7026
|
+
/** Tell the settings picker the provider list moved (catalog arrived, changed). */
|
|
7027
|
+
function notifyProviders() {
|
|
7028
|
+
if (providerListeners.length === 0) return
|
|
7029
|
+
var list = modelProviders()
|
|
7030
|
+
var listeners = providerListeners.slice()
|
|
7031
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
7032
|
+
try { listeners[i](list) } catch (error) { /* one listener must not block the rest */ }
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
|
|
5614
7036
|
/**
|
|
5615
7037
|
* Start the host's model catalog load as soon as a session resolves, rather
|
|
5616
7038
|
* than waiting for the first popover open.
|
|
@@ -5671,34 +7093,58 @@ window.__ModuleLoader__.load({
|
|
|
5671
7093
|
}
|
|
5672
7094
|
|
|
5673
7095
|
/**
|
|
5674
|
-
* The
|
|
5675
|
-
*
|
|
5676
|
-
*
|
|
5677
|
-
*
|
|
5678
|
-
* text. Family rules are ordered and anchored (see
|
|
5679
|
-
* src/model-descriptions.json) so another vendor's flash tier never
|
|
5680
|
-
* borrows DeepSeek's copy. A model this table has never seen and the
|
|
5681
|
-
* catalog does not describe resolves to an empty string on purpose: a
|
|
5682
|
-
* name-only row beats an invented line.
|
|
7096
|
+
* The effort slider (src/overrides/model-effort.js). It is handed a reader
|
|
7097
|
+
* rather than the seat itself: the control re-reads the catalog on every
|
|
7098
|
+
* pass, so a selection the host echoes back lands on the knob without the
|
|
7099
|
+
* picker having to push it.
|
|
5683
7100
|
*/
|
|
5684
|
-
function
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
7101
|
+
function effortControlElement() {
|
|
7102
|
+
if (effortSlider === null) {
|
|
7103
|
+
effortSlider = createEffortControl({
|
|
7104
|
+
read: function () { return modelEffort(modelSnapshot()) },
|
|
7105
|
+
onPick: pickEffort,
|
|
7106
|
+
// A drag must not be cut short by the hover-close timer: the pointer
|
|
7107
|
+
// is inside the control the whole time.
|
|
7108
|
+
onDragStart: cancelCloseModel,
|
|
7109
|
+
})
|
|
7110
|
+
}
|
|
7111
|
+
return effortSlider.el
|
|
7112
|
+
}
|
|
7113
|
+
|
|
7114
|
+
/** Re-point the slider at the current seat (every render pass). */
|
|
7115
|
+
function updateEffortControl() {
|
|
7116
|
+
if (effortSlider !== null) effortSlider.update()
|
|
5690
7117
|
}
|
|
5691
7118
|
|
|
5692
7119
|
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
7120
|
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
7121
|
|
|
7122
|
+
/** Catalog order is whatever the provider happened to send; id order is scannable. */
|
|
7123
|
+
function byModelId(a, b) {
|
|
7124
|
+
var left = String(a.id)
|
|
7125
|
+
var right = String(b.id)
|
|
7126
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
/**
|
|
7130
|
+
* The rule that separates one provider's models from the next. The provider
|
|
7131
|
+
* name rides the rule itself rather than trailing the model in parentheses:
|
|
7132
|
+
* one quiet line above the group says who serves it, and the model names
|
|
7133
|
+
* stay clean.
|
|
7134
|
+
*/
|
|
7135
|
+
function buildProviderRule(name) {
|
|
7136
|
+
var rule = modelEl('div', 'dsh-claude-model-rule')
|
|
7137
|
+
if (name) rule.appendChild(modelEl('span', 'dsh-claude-model-rule-name', name))
|
|
7138
|
+
return rule
|
|
7139
|
+
}
|
|
7140
|
+
|
|
5695
7141
|
/** One selectable model row: brand mark, name, optional description line and a check when current. */
|
|
5696
7142
|
function buildModelOption(group, model, selected, withDescription) {
|
|
5697
7143
|
var item = modelEl('button', 'dsh-claude-model-option')
|
|
5698
7144
|
item.type = 'button'
|
|
5699
7145
|
item.setAttribute('role', 'menuitemradio')
|
|
5700
7146
|
item.setAttribute('aria-checked', selected ? 'true' : 'false')
|
|
5701
|
-
var brand = modelBrand(
|
|
7147
|
+
var brand = modelBrand(model.id)
|
|
5702
7148
|
// The brand id is the row's styling hook — it is what gives a vendor's rows
|
|
5703
7149
|
// their own typography (see .dsh-claude-model-name in
|
|
5704
7150
|
// styles/components/model-picker.css). The vendor's mark is no longer drawn
|
|
@@ -5712,7 +7158,7 @@ window.__ModuleLoader__.load({
|
|
|
5712
7158
|
// while "More models" is every provider's full catalog and reads better
|
|
5713
7159
|
// as names alone. One line, in the shell's language — the copy document is
|
|
5714
7160
|
// localized rather than stacked, so a row never carries two languages.
|
|
5715
|
-
var desc = withDescription ? modelDescription(group.id, model) : ''
|
|
7161
|
+
var desc = withDescription ? modelDescription(ctx, group.id, model) : ''
|
|
5716
7162
|
if (desc) copy.appendChild(modelEl('span', 'dsh-claude-model-desc', desc))
|
|
5717
7163
|
item.appendChild(copy)
|
|
5718
7164
|
var check = modelEl('span', 'dsh-claude-model-check')
|
|
@@ -5727,28 +7173,27 @@ window.__ModuleLoader__.load({
|
|
|
5727
7173
|
return item
|
|
5728
7174
|
}
|
|
5729
7175
|
|
|
5730
|
-
/**
|
|
5731
|
-
function buildModelCell(label
|
|
7176
|
+
/** The More-models row: label + chevron, hover opens the second level. */
|
|
7177
|
+
function buildModelCell(label) {
|
|
5732
7178
|
var cell = modelEl('button', 'dsh-claude-model-cell')
|
|
5733
7179
|
cell.type = 'button'
|
|
5734
7180
|
cell.setAttribute('role', 'menuitem')
|
|
5735
7181
|
cell.appendChild(modelEl('span', 'dsh-claude-model-cell-label', label))
|
|
5736
|
-
if (value) cell.appendChild(modelEl('span', 'dsh-claude-model-cell-value', value))
|
|
5737
7182
|
var chevron = modelEl('span', 'dsh-claude-model-cell-chevron')
|
|
5738
7183
|
chevron.innerHTML = MODEL_CHEVRON_SVG
|
|
5739
7184
|
cell.appendChild(chevron)
|
|
5740
|
-
cell.addEventListener('mouseenter',
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
})
|
|
7185
|
+
cell.addEventListener('mouseenter', function () {
|
|
7186
|
+
if (readPrefs().autoPopover === AUTO_POPOVER_ALL) modelSubHoverIntent.scheduleOpen()
|
|
7187
|
+
})
|
|
7188
|
+
cell.addEventListener('mouseleave', function () {
|
|
7189
|
+
// A pointer that only crossed the cell must not drill in behind it.
|
|
7190
|
+
modelSubHoverIntent.cancel()
|
|
7191
|
+
})
|
|
7192
|
+
cell.addEventListener('click', function (e) {
|
|
7193
|
+
e.stopPropagation()
|
|
7194
|
+
if (modelSubPop && modelSubPop.getAttribute('data-open') === 'true') closeModelPopovers()
|
|
7195
|
+
else openModelSub()
|
|
7196
|
+
})
|
|
5752
7197
|
return cell
|
|
5753
7198
|
}
|
|
5754
7199
|
|
|
@@ -5764,6 +7209,7 @@ window.__ModuleLoader__.load({
|
|
|
5764
7209
|
closeModelPopovers()
|
|
5765
7210
|
}
|
|
5766
7211
|
|
|
7212
|
+
/** Commit one reasoning level. The slider stays open for the next nudge. */
|
|
5767
7213
|
function pickEffort(effort) {
|
|
5768
7214
|
var dir = modelDirectory()
|
|
5769
7215
|
var snap = modelSnapshot()
|
|
@@ -5774,73 +7220,137 @@ window.__ModuleLoader__.load({
|
|
|
5774
7220
|
var pending = dir.select(selection)
|
|
5775
7221
|
if (pending && typeof pending.catch === 'function') pending.catch(function () {})
|
|
5776
7222
|
} catch (error) { /* rejected selections surface on the host's toast */ }
|
|
5777
|
-
closeModelPopovers()
|
|
5778
7223
|
}
|
|
5779
7224
|
|
|
5780
|
-
/**
|
|
7225
|
+
/**
|
|
7226
|
+
* The providers level 1 lists: the official service first, then the quick
|
|
7227
|
+
* providers the settings page picked. Only when the catalog has no
|
|
7228
|
+
* (non-empty) official service at all does the list fall back to the
|
|
7229
|
+
* picked providers, and with none picked to every provider.
|
|
7230
|
+
*/
|
|
7231
|
+
function levelOneSections(groups) {
|
|
7232
|
+
var sections = []
|
|
7233
|
+
var chosen = readPrefs().quickProviders
|
|
7234
|
+
for (var g0 = 0; g0 < groups.length; g0++) {
|
|
7235
|
+
if (groups[g0].id === MODEL_OFFICIAL_GROUP && groups[g0].models.length > 0) {
|
|
7236
|
+
sections.push(groups[g0])
|
|
7237
|
+
break
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
for (var g2 = 0; g2 < groups.length; g2++) {
|
|
7241
|
+
if (chosen.indexOf(groups[g2].id) === -1 || groups[g2].id === MODEL_OFFICIAL_GROUP || groups[g2].models.length === 0) continue
|
|
7242
|
+
sections.push(groups[g2])
|
|
7243
|
+
}
|
|
7244
|
+
if (sections.length === 0) {
|
|
7245
|
+
for (var g4 = 0; g4 < groups.length; g4++) {
|
|
7246
|
+
if (groups[g4].models.length > 0) sections.push(groups[g4])
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
return sections
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7252
|
+
/**
|
|
7253
|
+
* The provider groups level 1 does NOT show — which is exactly what level 2
|
|
7254
|
+
* is for. Repeating a provider across the two cards made the same models
|
|
7255
|
+
* appear twice, one card apart.
|
|
7256
|
+
*
|
|
7257
|
+
* Only a GROUP counts as shown. The seat level 1 surfaces as a row of its
|
|
7258
|
+
* own does not: that row carries one model, not the provider, so hiding the
|
|
7259
|
+
* provider's remaining models behind it would strand them.
|
|
7260
|
+
*/
|
|
7261
|
+
function remainingGroups(groups, sections) {
|
|
7262
|
+
var shown = {}
|
|
7263
|
+
for (var i = 0; i < sections.length; i++) shown[sections[i].id] = true
|
|
7264
|
+
var out = []
|
|
7265
|
+
for (var g = 0; g < groups.length; g++) {
|
|
7266
|
+
if (groups[g].models.length === 0 || shown[groups[g].id] === true) continue
|
|
7267
|
+
out.push(groups[g])
|
|
7268
|
+
}
|
|
7269
|
+
return out
|
|
7270
|
+
}
|
|
7271
|
+
|
|
7272
|
+
/** Level 1: the provider sections, the divider, the effort slider, More models. */
|
|
5781
7273
|
function renderModelBody() {
|
|
5782
7274
|
if (!modelBody) return
|
|
7275
|
+
// A drag in flight owns the slider: rebuilding the footer would detach it
|
|
7276
|
+
// and drop its pointer capture mid-gesture. The pass that ends the drag —
|
|
7277
|
+
// settle commits, the host answers, a pass is scheduled — picks it up.
|
|
7278
|
+
if (effortSlider !== null && effortSlider.isDragging()) return
|
|
5783
7279
|
var snap = modelSnapshot()
|
|
5784
7280
|
var status = snap ? snap.status : 'idle'
|
|
5785
7281
|
var groups = (snap && snap.groups) || []
|
|
5786
7282
|
var current = modelCurrent(snap)
|
|
5787
7283
|
var effort = modelEffort(snap)
|
|
5788
|
-
var sig = [status, activeLocale(), current ? current.group.id + '/' + current.model.id : '', effort ? String(effort.effective) : ''].join('|')
|
|
7284
|
+
var sig = [status, activeLocale(), current ? current.group.id + '/' + current.model.id : '', effort ? String(effort.effective) : '', readPrefs().quickProviders.join(',')].join('|')
|
|
5789
7285
|
for (var g = 0; g < groups.length; g++) sig += ';' + groups[g].id + ':' + groups[g].models.length
|
|
5790
|
-
if (sig === modelBodySig)
|
|
7286
|
+
if (sig === modelBodySig) {
|
|
7287
|
+
// The list is unchanged, but the slider still has to follow the seat:
|
|
7288
|
+
// its geometry tracks the card's width, its value a selection the host
|
|
7289
|
+
// echoed back.
|
|
7290
|
+
updateEffortControl()
|
|
7291
|
+
return
|
|
7292
|
+
}
|
|
5791
7293
|
modelBodySig = sig
|
|
5792
7294
|
while (modelBody.firstChild) modelBody.removeChild(modelBody.firstChild)
|
|
5793
|
-
|
|
5794
|
-
|
|
7295
|
+
while (modelFooter && modelFooter.firstChild) modelFooter.removeChild(modelFooter.firstChild)
|
|
7296
|
+
|
|
7297
|
+
// A seat whose data is still in flight must not blank a picker that
|
|
7298
|
+
// already has a list. The host marks the directory `selecting` for the
|
|
7299
|
+
// WHOLE selectModel round-trip, and that round-trip runs for seconds on
|
|
7300
|
+
// providers whose adapters resolve over the network — the effort slider
|
|
7301
|
+
// commits through the very same RPC, so nudging it used to empty the
|
|
7302
|
+
// card (list, slider and More-models row all gone) for exactly as long
|
|
7303
|
+
// as the host took to answer. Only a directory with nothing to show
|
|
7304
|
+
// yet falls back to the loading line.
|
|
7305
|
+
var seated = groups.length > 0 && current !== null
|
|
7306
|
+
if (!seated && (status === 'idle' || status === 'loading' || status === 'selecting')) {
|
|
5795
7307
|
modelBody.appendChild(modelEl('div', 'dsh-claude-model-status', copyLabel('loading', MODEL_LOADING_LABEL)))
|
|
5796
7308
|
} else {
|
|
5797
|
-
var
|
|
5798
|
-
|
|
5799
|
-
if (groups[g2].id === MODEL_OFFICIAL_GROUP) { official = groups[g2]; break }
|
|
5800
|
-
}
|
|
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] })
|
|
5807
|
-
}
|
|
5808
|
-
}
|
|
5809
|
-
if (rows.length === 0) {
|
|
7309
|
+
var sections = levelOneSections(groups)
|
|
7310
|
+
if (sections.length === 0) {
|
|
5810
7311
|
modelBody.appendChild(modelEl('div', 'dsh-claude-model-status', copyLabel('empty', MODEL_EMPTY_LABEL)))
|
|
5811
7312
|
} else {
|
|
5812
|
-
for (var
|
|
5813
|
-
var
|
|
5814
|
-
|
|
7313
|
+
for (var s = 0; s < sections.length; s++) {
|
|
7314
|
+
var section = sections[s]
|
|
7315
|
+
// The official source needs no naming, and the first section needs
|
|
7316
|
+
// no rule: a bare line above the list would be one line too many.
|
|
7317
|
+
var sectionLabel = section.id === MODEL_OFFICIAL_GROUP ? '' : (section.name || section.id)
|
|
7318
|
+
if (sectionLabel !== '' || s > 0) modelBody.appendChild(buildProviderRule(sectionLabel))
|
|
7319
|
+
var sectionModels = section.models.slice().sort(byModelId)
|
|
7320
|
+
for (var m = 0; m < sectionModels.length; m++) {
|
|
7321
|
+
var selected = current !== null && current.group.id === section.id && current.model.id === sectionModels[m].id
|
|
7322
|
+
modelBody.appendChild(buildModelOption(section, sectionModels[m], selected, true))
|
|
7323
|
+
}
|
|
5815
7324
|
}
|
|
5816
7325
|
}
|
|
5817
|
-
|
|
5818
|
-
//
|
|
5819
|
-
//
|
|
5820
|
-
//
|
|
5821
|
-
//
|
|
5822
|
-
//
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
7326
|
+
// The current seat is surfaced under the list when none of the sections
|
|
7327
|
+
// above already carries it, so the row the seat is read from is always
|
|
7328
|
+
// on screen. Its provider rides a rule of its own rather than trailing
|
|
7329
|
+
// the model in parentheses — the same idiom the sections use. It stays
|
|
7330
|
+
// above the divider: the divider closes the model list, so anything
|
|
7331
|
+
// that belongs to the list has to sit on its side of it.
|
|
7332
|
+
var currentListed = false
|
|
7333
|
+
for (var c = 0; c < sections.length; c++) {
|
|
7334
|
+
if (current !== null && sections[c].id === current.group.id) currentListed = true
|
|
7335
|
+
}
|
|
7336
|
+
if (current !== null && !currentListed) {
|
|
7337
|
+
// The divider above already draws a line, so a provider that needs no
|
|
7338
|
+
// naming (the official source) adds nothing here.
|
|
7339
|
+
var currentRuleName = current.group.id === MODEL_OFFICIAL_GROUP ? '' : (current.group.name || current.group.id)
|
|
7340
|
+
if (currentRuleName !== '') modelBody.appendChild(buildProviderRule(currentRuleName))
|
|
5827
7341
|
var currentRow = modelEl('button', 'dsh-claude-model-option')
|
|
5828
7342
|
currentRow.type = 'button'
|
|
5829
7343
|
currentRow.setAttribute('role', 'menuitemradio')
|
|
5830
7344
|
currentRow.setAttribute('aria-checked', 'true')
|
|
5831
|
-
var currentBrand = modelBrand(current.
|
|
7345
|
+
var currentBrand = modelBrand(current.model.id)
|
|
5832
7346
|
var currentName = current.model.name || current.model.id
|
|
5833
7347
|
// The brand id is the row's styling hook here too, so this row wears
|
|
5834
7348
|
// the same vendor lockup and face as the list entry it stands for.
|
|
5835
7349
|
if (currentBrand) currentRow.setAttribute('data-brand', currentBrand)
|
|
5836
7350
|
var currentCopy = modelEl('span', 'dsh-claude-model-copy')
|
|
5837
7351
|
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
7352
|
currentCopy.appendChild(currentLabel)
|
|
5843
|
-
var currentDesc = modelDescription(current.group.id, current.model)
|
|
7353
|
+
var currentDesc = modelDescription(ctx, current.group.id, current.model)
|
|
5844
7354
|
if (currentDesc) currentCopy.appendChild(modelEl('span', 'dsh-claude-model-desc', currentDesc))
|
|
5845
7355
|
currentRow.appendChild(currentCopy)
|
|
5846
7356
|
var currentCheck = modelEl('span', 'dsh-claude-model-check')
|
|
@@ -5852,60 +7362,51 @@ window.__ModuleLoader__.load({
|
|
|
5852
7362
|
})
|
|
5853
7363
|
modelBody.appendChild(currentRow)
|
|
5854
7364
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
7365
|
+
// The divider closes the model list and the two drill rows follow it;
|
|
7366
|
+
// all three live in the footer, OUTSIDE the scroll area — the list
|
|
7367
|
+
// above scrolls under them while the controls stay reachable.
|
|
7368
|
+
if (modelFooter) {
|
|
7369
|
+
// The slider belongs to models that actually offer levels: without
|
|
7370
|
+
// reasoning metadata there is nothing to configure, so the row is not
|
|
7371
|
+
// drawn at all instead of being drawn inert.
|
|
7372
|
+
var showEffort = effort !== null
|
|
7373
|
+
// "More models" carries what level 1 does not. With every provider
|
|
7374
|
+
// already on screen the row would only open an empty card, so it goes
|
|
7375
|
+
// away with the last remaining provider.
|
|
7376
|
+
var showMore = remainingGroups(groups, sections).length > 0
|
|
7377
|
+
// The divider exists to close the list off from what follows it. With
|
|
7378
|
+
// both rows gone there is nothing left to close off, and a bare line
|
|
7379
|
+
// under the list reads as a stray rule.
|
|
7380
|
+
if (showEffort || showMore) {
|
|
7381
|
+
modelFooter.appendChild(modelEl('div', 'dsh-claude-model-divider'))
|
|
7382
|
+
if (showEffort) modelFooter.appendChild(effortControlElement())
|
|
7383
|
+
if (showMore) modelFooter.appendChild(buildModelCell(copyLabel('moreLabel', MODEL_MORE_LABEL)))
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
5857
7386
|
}
|
|
7387
|
+
updateEffortControl()
|
|
5858
7388
|
}
|
|
5859
7389
|
|
|
5860
|
-
/** Level 2: the
|
|
7390
|
+
/** Level 2: the providers level 1 does NOT show, each headed by its name. */
|
|
5861
7391
|
function renderModelSub() {
|
|
5862
7392
|
if (!modelSubBody) return
|
|
5863
7393
|
var snap = modelSnapshot()
|
|
5864
|
-
if (modelSubKind === 'effort') {
|
|
5865
|
-
var effort = modelEffort(snap)
|
|
5866
|
-
var sig = 'effort:' + (effort ? String(effort.effective) : 'none')
|
|
5867
|
-
if (sig === modelSubSig) return
|
|
5868
|
-
modelSubSig = sig
|
|
5869
|
-
while (modelSubBody.firstChild) modelSubBody.removeChild(modelSubBody.firstChild)
|
|
5870
|
-
if (effort === null) {
|
|
5871
|
-
modelSubBody.appendChild(modelEl('div', 'dsh-claude-model-status', copyLabel('noEffort', MODEL_NO_EFFORT_LABEL)))
|
|
5872
|
-
return
|
|
5873
|
-
}
|
|
5874
|
-
var levels = []
|
|
5875
|
-
if (effort.reasoning.defaultEffort === void 0) levels.push({ effort: void 0, label: MODEL_EFFORT_DEFAULT })
|
|
5876
|
-
for (var i = 0; i < effort.reasoning.efforts.length; i++) {
|
|
5877
|
-
levels.push({ effort: effort.reasoning.efforts[i].id, label: effort.reasoning.efforts[i].name })
|
|
5878
|
-
}
|
|
5879
|
-
for (var l = 0; l < levels.length; l++) {
|
|
5880
|
-
(function (level, active) {
|
|
5881
|
-
var item = modelEl('button', 'dsh-claude-model-option')
|
|
5882
|
-
item.type = 'button'
|
|
5883
|
-
item.setAttribute('role', 'menuitemradio')
|
|
5884
|
-
item.setAttribute('aria-checked', active ? 'true' : 'false')
|
|
5885
|
-
item.appendChild(modelEl('span', 'dsh-claude-model-copy', level.label))
|
|
5886
|
-
var check = modelEl('span', 'dsh-claude-model-check')
|
|
5887
|
-
check.innerHTML = active ? MODEL_CHECK_SVG : ''
|
|
5888
|
-
item.appendChild(check)
|
|
5889
|
-
item.addEventListener('click', function (e) {
|
|
5890
|
-
e.stopPropagation()
|
|
5891
|
-
pickEffort(level.effort)
|
|
5892
|
-
})
|
|
5893
|
-
modelSubBody.appendChild(item)
|
|
5894
|
-
})(levels[l], effort.effective === levels[l].effort)
|
|
5895
|
-
}
|
|
5896
|
-
return
|
|
5897
|
-
}
|
|
5898
|
-
// 'more': every provider group, headed by its name.
|
|
5899
7394
|
var groups = (snap && snap.groups) || []
|
|
5900
7395
|
var current = modelCurrent(snap)
|
|
5901
|
-
|
|
7396
|
+
// What level 2 holds depends on what level 1 lists, so the signature has
|
|
7397
|
+
// to carry level 1's provider ids as well.
|
|
7398
|
+
var sections = levelOneSections(groups)
|
|
7399
|
+
var listed = []
|
|
7400
|
+
for (var s0 = 0; s0 < sections.length; s0++) listed.push(sections[s0].id)
|
|
7401
|
+
var sig2 = 'more|' + listed.join(',')
|
|
5902
7402
|
for (var g = 0; g < groups.length; g++) sig2 += ';' + groups[g].id + ':' + groups[g].models.length
|
|
5903
7403
|
if (current) sig2 += '#' + current.group.id + '/' + current.model.id
|
|
5904
7404
|
if (sig2 === modelSubSig) return
|
|
5905
7405
|
modelSubSig = sig2
|
|
5906
7406
|
while (modelSubBody.firstChild) modelSubBody.removeChild(modelSubBody.firstChild)
|
|
5907
|
-
|
|
5908
|
-
|
|
7407
|
+
var rest = remainingGroups(groups, sections)
|
|
7408
|
+
for (var g2 = 0; g2 < rest.length; g2++) {
|
|
7409
|
+
var group = rest[g2]
|
|
5909
7410
|
if (group.models.length === 0) continue
|
|
5910
7411
|
var groupSection = modelEl('div', 'dsh-claude-model-group-section')
|
|
5911
7412
|
var groupRow = modelEl('div', 'dsh-claude-model-group-row')
|
|
@@ -5918,11 +7419,7 @@ window.__ModuleLoader__.load({
|
|
|
5918
7419
|
// A provider's models read in id order, so the list is scannable and stays
|
|
5919
7420
|
// put between visits; the catalog's own order is whatever the provider
|
|
5920
7421
|
// happened to send. Sorted on a copy — the snapshot belongs to the store.
|
|
5921
|
-
var groupModels = group.models.slice().sort(
|
|
5922
|
-
var left = String(a.id)
|
|
5923
|
-
var right = String(b.id)
|
|
5924
|
-
return left < right ? -1 : left > right ? 1 : 0
|
|
5925
|
-
})
|
|
7422
|
+
var groupModels = group.models.slice().sort(byModelId)
|
|
5926
7423
|
for (var m = 0; m < groupModels.length; m++) {
|
|
5927
7424
|
var selected = current !== null && current.group.id === group.id && current.model.id === groupModels[m].id
|
|
5928
7425
|
groupSection.appendChild(buildModelOption(group, groupModels[m], selected, false))
|
|
@@ -5935,22 +7432,39 @@ window.__ModuleLoader__.load({
|
|
|
5935
7432
|
}
|
|
5936
7433
|
|
|
5937
7434
|
function positionModelPopovers() {
|
|
7435
|
+
trackPointerWhileOpen()
|
|
5938
7436
|
if (!modelBtn || !modelPop) return
|
|
5939
7437
|
var pos = positionAnchoredPopover(modelBtn, modelPop, { side: 'above', gap: 6 })
|
|
5940
7438
|
if (modelSubPop && modelSubPop.getAttribute('data-open') === 'true') {
|
|
5941
7439
|
var w2 = modelSubPop.offsetWidth
|
|
5942
7440
|
var h2 = modelSubPop.offsetHeight
|
|
7441
|
+
var h1 = modelPop.offsetHeight
|
|
5943
7442
|
// Beside the first level; flip to its left when the viewport is tight.
|
|
5944
|
-
var x2 = pos.x + modelPop.offsetWidth +
|
|
7443
|
+
var x2 = pos.x + modelPop.offsetWidth + 2
|
|
5945
7444
|
if (x2 + w2 > window.innerWidth - POPOVER_MARGIN) x2 = Math.max(POPOVER_MARGIN, pos.x - 4 - w2)
|
|
5946
|
-
|
|
7445
|
+
// Bottom-aligned with the first level: a short level 2 used to hang from
|
|
7446
|
+
// the top, leaving dead space under it that the pointer had to cross to
|
|
7447
|
+
// reach the card from the More-models row. A level 2 TALLER than level 1
|
|
7448
|
+
// keeps its top instead, so it cannot push itself off the top edge.
|
|
7449
|
+
var y2 = pos.y + Math.max(0, h1 - h2)
|
|
7450
|
+
y2 = Math.max(POPOVER_MARGIN, Math.min(y2, window.innerHeight - h2 - POPOVER_MARGIN))
|
|
5947
7451
|
modelSubPop.style.left = x2 + 'px'
|
|
5948
7452
|
modelSubPop.style.top = y2 + 'px'
|
|
5949
7453
|
}
|
|
5950
7454
|
}
|
|
5951
7455
|
|
|
5952
7456
|
function ensureModelChrome() {
|
|
5953
|
-
|
|
7457
|
+
// Idempotence guard, not just a null check: the picker's own teardown
|
|
7458
|
+
// sweep, or a hot-reload generation's stray sweep, can REMOVE the
|
|
7459
|
+
// popover node while the closure still references it. With a null-only
|
|
7460
|
+
// check the reference stays non-null-but-detached and is never rebuilt
|
|
7461
|
+
// — the trigger then toggles a popover that is not in the document and
|
|
7462
|
+
// the picker silently loses its click effect. The account footer's
|
|
7463
|
+
// equivalent guard (footArea.contains) is the established pattern.
|
|
7464
|
+
// Rebuilding also re-points the body/footer children and resets the
|
|
7465
|
+
// render signatures so the next pass repaints into the fresh nodes.
|
|
7466
|
+
if (modelPop === null || modelPop.parentElement === null) {
|
|
7467
|
+
if (modelPop !== null && modelPop.parentElement !== null) modelPop.parentElement.removeChild(modelPop)
|
|
5954
7468
|
modelPop = document.createElement('div')
|
|
5955
7469
|
modelPop.className = 'dsh-claude-model-popover'
|
|
5956
7470
|
modelPop.setAttribute('role', 'menu')
|
|
@@ -5958,11 +7472,43 @@ window.__ModuleLoader__.load({
|
|
|
5958
7472
|
modelBody = document.createElement('div')
|
|
5959
7473
|
modelBody.className = 'dsh-claude-model-popover-body'
|
|
5960
7474
|
modelPop.appendChild(modelBody)
|
|
7475
|
+
modelFooter = document.createElement('div')
|
|
7476
|
+
modelFooter.className = 'dsh-claude-model-footer'
|
|
7477
|
+
modelPop.appendChild(modelFooter)
|
|
5961
7478
|
modelPop.addEventListener('mouseenter', cancelCloseModel)
|
|
5962
|
-
|
|
7479
|
+
// The second level belongs to its More-models cell: while it is open,
|
|
7480
|
+
// the pointer landing anywhere else on the first level (the effort
|
|
7481
|
+
// slider, a model row, bare card) folds it — the first level itself
|
|
7482
|
+
// stays open, it is the hover-intent host. Delegated mouseover, not
|
|
7483
|
+
// mouseenter: moving from the cell to the slider never crosses the
|
|
7484
|
+
// popover's boundary, so a boundary event would never fire.
|
|
7485
|
+
modelPop.addEventListener('mouseover', function (e) {
|
|
7486
|
+
if (modelSubPop === null || modelSubPop.getAttribute('data-open') !== 'true') return
|
|
7487
|
+
var target = e.target
|
|
7488
|
+
if (target && typeof target.closest === 'function' && target.closest('.dsh-claude-model-cell')) return
|
|
7489
|
+
// Delayed, not immediate: the sub card sits BESIDE level 1, so a
|
|
7490
|
+
// pointer on its way from the cell to the sub crosses level 1's own
|
|
7491
|
+
// rows — folding on the spot made that journey impossible at any
|
|
7492
|
+
// speed. Folding now waits out the same grace, and stands down if the
|
|
7493
|
+
// pointer has meanwhile reached either card.
|
|
7494
|
+
if (subFoldTimer !== null) clearTimeout(subFoldTimer)
|
|
7495
|
+
subFoldTimer = setTimeout(function () {
|
|
7496
|
+
subFoldTimer = null
|
|
7497
|
+
if (pointerInPicker()) return
|
|
7498
|
+
if (modelSubPop !== null) modelSubPop.setAttribute('data-open', 'false')
|
|
7499
|
+
}, MODEL_CLOSE_DELAY)
|
|
7500
|
+
})
|
|
7501
|
+
modelPop.addEventListener('mouseleave', function () {
|
|
7502
|
+
// A drag in flight must not be cut short by the hover-close timer: the
|
|
7503
|
+
// pointer is working the slider, not leaving the card.
|
|
7504
|
+
if (effortSlider !== null && effortSlider.isDragging()) return
|
|
7505
|
+
scheduleCloseModel()
|
|
7506
|
+
})
|
|
5963
7507
|
document.body.appendChild(modelPop)
|
|
7508
|
+
modelBodySig = ''
|
|
5964
7509
|
}
|
|
5965
|
-
if (modelSubPop === null) {
|
|
7510
|
+
if (modelSubPop === null || modelSubPop.parentElement === null) {
|
|
7511
|
+
if (modelSubPop !== null && modelSubPop.parentElement !== null) modelSubPop.parentElement.removeChild(modelSubPop)
|
|
5966
7512
|
modelSubPop = document.createElement('div')
|
|
5967
7513
|
modelSubPop.className = 'dsh-claude-model-popover dsh-claude-model-popover-sub'
|
|
5968
7514
|
modelSubPop.setAttribute('role', 'menu')
|
|
@@ -5973,12 +7519,16 @@ window.__ModuleLoader__.load({
|
|
|
5973
7519
|
modelSubPop.addEventListener('mouseenter', cancelCloseModel)
|
|
5974
7520
|
modelSubPop.addEventListener('mouseleave', scheduleCloseModel)
|
|
5975
7521
|
document.body.appendChild(modelSubPop)
|
|
7522
|
+
modelSubSig = ''
|
|
5976
7523
|
}
|
|
5977
7524
|
}
|
|
5978
7525
|
|
|
5979
7526
|
/** Build/refresh the trigger, its label and the popover rows. */
|
|
5980
7527
|
function syncModelControl() {
|
|
5981
|
-
|
|
7528
|
+
// Two ways to be off: the composer restyle does not apply to this page, or
|
|
7529
|
+
// the picker preference is off. Both hand the host's own model seat and
|
|
7530
|
+
// menu back, so both run the same sweep.
|
|
7531
|
+
if (!ui.copy.isComposerActive() || !readPrefs().modelPicker) {
|
|
5982
7532
|
var allHosts = document.querySelectorAll('[data-dsh-claude-model-host]')
|
|
5983
7533
|
for (var h = 0; h < allHosts.length; h++) {
|
|
5984
7534
|
allHosts[h].removeAttribute('data-dsh-claude-model-host')
|
|
@@ -5995,8 +7545,8 @@ window.__ModuleLoader__.load({
|
|
|
5995
7545
|
modelPop = null
|
|
5996
7546
|
modelSubPop = null
|
|
5997
7547
|
modelBody = null
|
|
7548
|
+
modelFooter = null
|
|
5998
7549
|
modelSubBody = null
|
|
5999
|
-
modelSubKind = null
|
|
6000
7550
|
modelBodySig = ''
|
|
6001
7551
|
modelSubSig = ''
|
|
6002
7552
|
cancelCloseModel()
|
|
@@ -6014,6 +7564,18 @@ window.__ModuleLoader__.load({
|
|
|
6014
7564
|
var slot = document.querySelector('[data-slot="conversation.input.model"]')
|
|
6015
7565
|
if (slot === null) return
|
|
6016
7566
|
// Hide the host's own seat (React owns the node; re-mark on swap).
|
|
7567
|
+
// Idempotent against a torn-down-less reload, like the account footer:
|
|
7568
|
+
// client HMR drops the old fiber's disposals, so a previous generation's
|
|
7569
|
+
// trigger and popovers are still in the DOM while this fresh scope starts
|
|
7570
|
+
// from null. Sweep the strays, or the seat renders twice.
|
|
7571
|
+
var strayBtns = slot.querySelectorAll('.dsh-claude-model-btn')
|
|
7572
|
+
for (var sb = 0; sb < strayBtns.length; sb++) {
|
|
7573
|
+
if (strayBtns[sb] !== modelBtn) strayBtns[sb].parentElement.removeChild(strayBtns[sb])
|
|
7574
|
+
}
|
|
7575
|
+
var strayPops = document.querySelectorAll('body > .dsh-claude-model-popover')
|
|
7576
|
+
for (var sp = 0; sp < strayPops.length; sp++) {
|
|
7577
|
+
if (strayPops[sp] !== modelPop && strayPops[sp] !== modelSubPop) strayPops[sp].parentElement.removeChild(strayPops[sp])
|
|
7578
|
+
}
|
|
6017
7579
|
var hostRoot = slot.firstElementChild
|
|
6018
7580
|
if (hostRoot !== null && !hostRoot.hasAttribute('data-dsh-claude-model-host')) {
|
|
6019
7581
|
hostRoot.setAttribute('data-dsh-claude-model-host', '')
|
|
@@ -6025,13 +7587,13 @@ window.__ModuleLoader__.load({
|
|
|
6025
7587
|
modelBtn.className = 'dsh-claude-model-btn'
|
|
6026
7588
|
modelBtn.setAttribute('aria-haspopup', 'menu')
|
|
6027
7589
|
modelBtn.innerHTML = '<span class="dsh-claude-model-btn-label"></span>'
|
|
6028
|
-
// Same contract as the account trigger: hover
|
|
6029
|
-
//
|
|
7590
|
+
// Same contract as the account trigger: hover under the "All" scope,
|
|
7591
|
+
// click-only otherwise.
|
|
6030
7592
|
modelBtn.addEventListener('mouseenter', function () {
|
|
6031
|
-
if (readPrefs().autoPopover)
|
|
7593
|
+
if (readPrefs().autoPopover === AUTO_POPOVER_ALL) modelHoverIntent.scheduleOpen()
|
|
6032
7594
|
})
|
|
6033
7595
|
modelBtn.addEventListener('mouseleave', function () {
|
|
6034
|
-
if (readPrefs().autoPopover) scheduleCloseModel()
|
|
7596
|
+
if (readPrefs().autoPopover === AUTO_POPOVER_ALL) scheduleCloseModel()
|
|
6035
7597
|
})
|
|
6036
7598
|
modelBtn.addEventListener('click', function (e) {
|
|
6037
7599
|
e.stopPropagation()
|
|
@@ -6045,11 +7607,24 @@ window.__ModuleLoader__.load({
|
|
|
6045
7607
|
var snap = modelSnapshot()
|
|
6046
7608
|
var current = modelCurrent(snap)
|
|
6047
7609
|
var effort = modelEffort(snap)
|
|
7610
|
+
var groupsNow = (snap && snap.groups) || []
|
|
6048
7611
|
var label = current ? current.model.name : copyLabel('fallbackLabel', MODEL_FALLBACK_LABEL)
|
|
6049
7612
|
var labelEl = modelBtn.querySelector('.dsh-claude-model-btn-label')
|
|
6050
7613
|
if (labelEl) {
|
|
6051
|
-
|
|
6052
|
-
|
|
7614
|
+
// Same-value guards: syncModelControl runs on every scheduler pass,
|
|
7615
|
+
// and an identical write still mutates the DOM (textContent replaces
|
|
7616
|
+
// the text node; setAttribute queues an attribute record — and
|
|
7617
|
+
// aria-label is in the observer's attributeFilter). Unguarded, each
|
|
7618
|
+
// pass feeds the observer that schedules the next pass, keeping one
|
|
7619
|
+
// full pass running every frame even at idle.
|
|
7620
|
+
if (labelEl.textContent !== label) labelEl.textContent = label
|
|
7621
|
+
// The dimmed tone means "no seat to name yet". A selection in flight
|
|
7622
|
+
// still names the model in force, so it keeps the normal tone — the
|
|
7623
|
+
// host can hold `selecting` for seconds (the effort slider commits
|
|
7624
|
+
// through that same RPC), and a greyed-out trigger for that long
|
|
7625
|
+
// reads as broken rather than busy.
|
|
7626
|
+
var unsettled = !!(snap && (snap.status === 'loading' || snap.status === 'idle' || snap.status === 'selecting')) && !(groupsNow.length > 0 && current !== null)
|
|
7627
|
+
labelEl.classList.toggle('dsh-claude-model-btn-loading', unsettled)
|
|
6053
7628
|
}
|
|
6054
7629
|
var effortEl = modelBtn.querySelector('.dsh-claude-model-btn-effort')
|
|
6055
7630
|
if (effort) {
|
|
@@ -6057,19 +7632,22 @@ window.__ModuleLoader__.load({
|
|
|
6057
7632
|
effortEl = modelEl('span', 'dsh-claude-model-btn-effort')
|
|
6058
7633
|
modelBtn.insertBefore(effortEl, modelBtn.firstChild ? labelEl.nextSibling : null)
|
|
6059
7634
|
}
|
|
6060
|
-
|
|
7635
|
+
var effortText = '· ' + effort.label
|
|
7636
|
+
if (effortEl.textContent !== effortText) effortEl.textContent = effortText
|
|
6061
7637
|
} else if (effortEl !== null && effortEl.parentElement) {
|
|
6062
7638
|
effortEl.parentElement.removeChild(effortEl)
|
|
6063
7639
|
}
|
|
6064
|
-
|
|
7640
|
+
var triggerAria = copyLabel('triggerLabel', MODEL_TRIGGER_LABEL, { model: label })
|
|
7641
|
+
if (modelBtn.getAttribute('aria-label') !== triggerAria) modelBtn.setAttribute('aria-label', triggerAria)
|
|
6065
7642
|
modelBtn.disabled = false
|
|
6066
7643
|
|
|
6067
7644
|
renderModelBody()
|
|
6068
|
-
if (
|
|
7645
|
+
if (modelSubPop && modelSubPop.getAttribute('data-open') === 'true') {
|
|
6069
7646
|
renderModelSub()
|
|
6070
|
-
|
|
7647
|
+
positionModelPopovers()
|
|
7648
|
+
} else if (modelPop && modelPop.getAttribute('data-open') === 'true') {
|
|
7649
|
+
positionModelPopovers()
|
|
6071
7650
|
}
|
|
6072
|
-
if (modelPop && modelPop.getAttribute('data-open') === 'true') positionModelPopovers()
|
|
6073
7651
|
}
|
|
6074
7652
|
|
|
6075
7653
|
|
|
@@ -6083,6 +7661,14 @@ window.__ModuleLoader__.load({
|
|
|
6083
7661
|
(modelSubPop !== null && modelSubPop.contains(target))
|
|
6084
7662
|
},
|
|
6085
7663
|
reposition: positionModelPopovers,
|
|
7664
|
+
providers: modelProviders,
|
|
7665
|
+
onProviders: function (listener) {
|
|
7666
|
+
providerListeners.push(listener)
|
|
7667
|
+
return function () {
|
|
7668
|
+
var at = providerListeners.indexOf(listener)
|
|
7669
|
+
if (at !== -1) providerListeners.splice(at, 1)
|
|
7670
|
+
}
|
|
7671
|
+
},
|
|
6086
7672
|
invalidateCopy: function () {
|
|
6087
7673
|
modelBodySig = ''
|
|
6088
7674
|
modelSubSig = ''
|
|
@@ -6097,8 +7683,9 @@ window.__ModuleLoader__.load({
|
|
|
6097
7683
|
modelPop = null
|
|
6098
7684
|
modelSubPop = null
|
|
6099
7685
|
modelBody = null
|
|
7686
|
+
modelFooter = null
|
|
7687
|
+
effortSlider = null
|
|
6100
7688
|
modelSubBody = null
|
|
6101
|
-
modelSubKind = null
|
|
6102
7689
|
modelBodySig = ''
|
|
6103
7690
|
modelSubSig = ''
|
|
6104
7691
|
}
|
|
@@ -6107,11 +7694,361 @@ window.__ModuleLoader__.load({
|
|
|
6107
7694
|
return ui.model.teardown
|
|
6108
7695
|
}
|
|
6109
7696
|
|
|
7697
|
+
/**
|
|
7698
|
+
* The hero row's two pickers — the workspace (directory) chip and the
|
|
7699
|
+
* agent-preset seat — both open the host's shared menu primitive, which
|
|
7700
|
+
* portals its card to <body>. That card carries no marker of its own, so a
|
|
7701
|
+
* stylesheet cannot tell it apart from the host's other menus (sidebar row
|
|
7702
|
+
* menus, the settings permission row, submenus) — and it has to: this skin
|
|
7703
|
+
* redraws these two and leaves the rest alone.
|
|
7704
|
+
*
|
|
7705
|
+
* So the browser half stamps the open card rather than guessing in CSS.
|
|
7706
|
+
* While either trigger reports `aria-expanded="true"`, the one host menu
|
|
7707
|
+
* sitting in <body> is that card; HERO_MENU_ATTR is what
|
|
7708
|
+
* components/hero-menu.css switches on.
|
|
7709
|
+
*
|
|
7710
|
+
* The scheduler's attributeFilter does not watch `aria-expanded`, but the
|
|
7711
|
+
* card is inserted into <body> as it opens — a childList mutation the pass
|
|
7712
|
+
* already observes — so the stamp lands on the frame the menu appears. Two
|
|
7713
|
+
* candidates mean an unrelated menu is open as well: the pass then leaves
|
|
7714
|
+
* every card alone rather than stamping the wrong one.
|
|
7715
|
+
*
|
|
7716
|
+
* The host drops that card below its trigger, which is exactly where the
|
|
7717
|
+
* composer sits: it would land on the input area and on the controls in it.
|
|
7718
|
+
* This row is *above* the composer, so the card belongs beside the trigger
|
|
7719
|
+
* instead — bottom-aligned with it and growing upward into the empty hero
|
|
7720
|
+
* space. The host re-places the card on every scroll and resize, so this
|
|
7721
|
+
* position is written with `important` (which beats its inline write) and
|
|
7722
|
+
* re-applied from the scheduler's scroll/resize hook.
|
|
7723
|
+
*
|
|
7724
|
+
* Hover is the host's own click handler driven from here: the "Open popovers
|
|
7725
|
+
* on hover" preference's `all` scope covers these two, and the host offers no
|
|
7726
|
+
* hover of its own. The listeners are delegated from the document because
|
|
7727
|
+
* React replaces the triggers, and the close waits out the gap between the
|
|
7728
|
+
* trigger and the card so crossing it does not shut the menu.
|
|
7729
|
+
*/
|
|
7730
|
+
function installHeroMenu(ctx, ui) {
|
|
7731
|
+
/** Air between the trigger and the card, and the viewport's own margin. */
|
|
7732
|
+
var GAP = 6
|
|
7733
|
+
var MARGIN = 12
|
|
7734
|
+
/** Long enough to cross the gap above, short enough to still read as hover. */
|
|
7735
|
+
var CLOSE_DELAY = POPOVER_CLOSE_DELAY
|
|
7736
|
+
/** The two triggers, and the card once it is stamped. */
|
|
7737
|
+
var TRIGGER_SELECTOR = '[class*="heroWorkspaceRow"] [aria-haspopup="menu"]'
|
|
7738
|
+
var CARD_SELECTOR = '[' + HERO_MENU_ATTR + ']'
|
|
7739
|
+
var stamped = null
|
|
7740
|
+
var stampedTrigger = null
|
|
7741
|
+
var closeTimer = null
|
|
7742
|
+
var openTimer = null
|
|
7743
|
+
var openedByHover = false
|
|
7744
|
+
|
|
7745
|
+
function cancelHoverClose() {
|
|
7746
|
+
if (closeTimer === null) return
|
|
7747
|
+
clearTimeout(closeTimer)
|
|
7748
|
+
closeTimer = null
|
|
7749
|
+
}
|
|
7750
|
+
|
|
7751
|
+
function cancelHoverOpen() {
|
|
7752
|
+
if (openTimer === null) return
|
|
7753
|
+
clearTimeout(openTimer)
|
|
7754
|
+
openTimer = null
|
|
7755
|
+
}
|
|
7756
|
+
|
|
7757
|
+
/**
|
|
7758
|
+
* Open what hover asked for, once the pointer has stayed the dwell out.
|
|
7759
|
+
* The host's menu has no hover of its own — this clicks its trigger — so
|
|
7760
|
+
* the dwell is what keeps a pointer merely crossing the hero row from
|
|
7761
|
+
* unfolding the card.
|
|
7762
|
+
*/
|
|
7763
|
+
function openFromHover(trigger) {
|
|
7764
|
+
openTimer = null
|
|
7765
|
+
if (!hoverEnabled()) return
|
|
7766
|
+
if (trigger.getAttribute('aria-expanded') === 'true') return
|
|
7767
|
+
trigger.click()
|
|
7768
|
+
openedByHover = true
|
|
7769
|
+
}
|
|
7770
|
+
|
|
7771
|
+
function scheduleHoverOpen(trigger) {
|
|
7772
|
+
cancelHoverOpen()
|
|
7773
|
+
openTimer = setTimeout(function () { openFromHover(trigger) }, POPOVER_OPEN_DELAY)
|
|
7774
|
+
}
|
|
7775
|
+
|
|
7776
|
+
/** Close what hover opened; a click-opened menu is left alone. */
|
|
7777
|
+
function closeFromHover() {
|
|
7778
|
+
closeTimer = null
|
|
7779
|
+
if (!openedByHover) return
|
|
7780
|
+
var trigger = openTrigger()
|
|
7781
|
+
if (trigger !== null) trigger.click()
|
|
7782
|
+
openedByHover = false
|
|
7783
|
+
}
|
|
7784
|
+
|
|
7785
|
+
function scheduleHoverClose() {
|
|
7786
|
+
cancelHoverClose()
|
|
7787
|
+
closeTimer = setTimeout(closeFromHover, CLOSE_DELAY)
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
/** The `all` scope only, and only while the composer restyle is in play. */
|
|
7791
|
+
function hoverEnabled() {
|
|
7792
|
+
return readPrefs().autoPopover === AUTO_POPOVER_ALL &&
|
|
7793
|
+
ui.copy !== undefined && ui.copy.isComposerActive()
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
function closestWithin(target, selector) {
|
|
7797
|
+
if (target === null || target === undefined || typeof target.closest !== 'function') return null
|
|
7798
|
+
return target.closest(selector)
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
function onHeroPointerOver(e) {
|
|
7802
|
+
if (!hoverEnabled()) return
|
|
7803
|
+
var target = e.target
|
|
7804
|
+
if (closestWithin(target, CARD_SELECTOR) !== null) {
|
|
7805
|
+
cancelHoverClose()
|
|
7806
|
+
return
|
|
7807
|
+
}
|
|
7808
|
+
var trigger = closestWithin(target, TRIGGER_SELECTOR)
|
|
7809
|
+
if (trigger === null) return
|
|
7810
|
+
cancelHoverClose()
|
|
7811
|
+
if (trigger.getAttribute('aria-expanded') !== 'true') scheduleHoverOpen(trigger)
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7814
|
+
function onHeroPointerOut(e) {
|
|
7815
|
+
if (!hoverEnabled()) return
|
|
7816
|
+
var target = e.target
|
|
7817
|
+
if (closestWithin(target, CARD_SELECTOR) === null && closestWithin(target, TRIGGER_SELECTOR) === null) return
|
|
7818
|
+
// Moving onto the other half — the card, or the trigger — is not a leave.
|
|
7819
|
+
var next = e.relatedTarget
|
|
7820
|
+
if (closestWithin(next, CARD_SELECTOR) !== null || closestWithin(next, TRIGGER_SELECTOR) !== null) return
|
|
7821
|
+
cancelHoverOpen()
|
|
7822
|
+
scheduleHoverClose()
|
|
7823
|
+
}
|
|
7824
|
+
|
|
7825
|
+
document.addEventListener('mouseover', onHeroPointerOver, true)
|
|
7826
|
+
document.addEventListener('mouseout', onHeroPointerOut, true)
|
|
7827
|
+
|
|
7828
|
+
function clearStamp() {
|
|
7829
|
+
if (stamped === null) return
|
|
7830
|
+
stamped.removeAttribute(HERO_MENU_ATTR)
|
|
7831
|
+
stamped = null
|
|
7832
|
+
stampedTrigger = null
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
/** The trigger whose picker is open, or null. */
|
|
7836
|
+
function openTrigger() {
|
|
7837
|
+
return document.querySelector('[class*="heroWorkspaceRow"] [aria-haspopup="menu"][aria-expanded="true"]') ||
|
|
7838
|
+
document.querySelector('[class*="cardWorkspaceTrigger"] [aria-expanded="true"]')
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7841
|
+
/**
|
|
7842
|
+
* Park the card beside its trigger: to its right, bottom-aligned so it
|
|
7843
|
+
* grows upward, flipped to the left when the viewport is tight, and clamped
|
|
7844
|
+
* to the viewport either way. A card that has not been laid out yet is left
|
|
7845
|
+
* for the next pass rather than pinned to a zero-sized guess.
|
|
7846
|
+
*/
|
|
7847
|
+
function placeCard(trigger, card) {
|
|
7848
|
+
var rect = trigger.getBoundingClientRect()
|
|
7849
|
+
var width = card.offsetWidth
|
|
7850
|
+
var height = card.offsetHeight
|
|
7851
|
+
if (width === 0 || height === 0) return
|
|
7852
|
+
var left = rect.right + GAP
|
|
7853
|
+
if (left + width > window.innerWidth - MARGIN) left = rect.left - GAP - width
|
|
7854
|
+
left = Math.max(MARGIN, Math.min(left, window.innerWidth - width - MARGIN))
|
|
7855
|
+
var top = rect.bottom - height
|
|
7856
|
+
top = Math.max(MARGIN, Math.min(top, window.innerHeight - height - MARGIN))
|
|
7857
|
+
card.style.setProperty('left', Math.round(left) + 'px', 'important')
|
|
7858
|
+
card.style.setProperty('top', Math.round(top) + 'px', 'important')
|
|
7859
|
+
}
|
|
7860
|
+
|
|
7861
|
+
/** Re-place an open card after a scroll or a resize moved its anchor. */
|
|
7862
|
+
function repositionHeroMenu() {
|
|
7863
|
+
if (stamped === null || stampedTrigger === null) return
|
|
7864
|
+
placeCard(stampedTrigger, stamped)
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7867
|
+
function syncHeroMenu() {
|
|
7868
|
+
var trigger = openTrigger()
|
|
7869
|
+
if (trigger === null) {
|
|
7870
|
+
// The menu is shut — by a click, by Escape or by an outside press — so
|
|
7871
|
+
// whatever hover opened it no longer owns it.
|
|
7872
|
+
openedByHover = false
|
|
7873
|
+
cancelHoverClose()
|
|
7874
|
+
clearStamp()
|
|
7875
|
+
return
|
|
7876
|
+
}
|
|
7877
|
+
// The skin's own popovers carry their own classes, so excluding them
|
|
7878
|
+
// leaves the host's menus only.
|
|
7879
|
+
var cards = document.querySelectorAll('body > [role="menu"]:not([class*="dsh-claude"])')
|
|
7880
|
+
if (cards.length !== 1) {
|
|
7881
|
+
clearStamp()
|
|
7882
|
+
return
|
|
7883
|
+
}
|
|
7884
|
+
if (stamped !== cards[0]) {
|
|
7885
|
+
clearStamp()
|
|
7886
|
+
stamped = cards[0]
|
|
7887
|
+
stampedTrigger = trigger
|
|
7888
|
+
stamped.setAttribute(HERO_MENU_ATTR, '')
|
|
7889
|
+
}
|
|
7890
|
+
placeCard(trigger, stamped)
|
|
7891
|
+
}
|
|
7892
|
+
|
|
7893
|
+
ui.heroMenu = { sync: syncHeroMenu, reposition: repositionHeroMenu }
|
|
7894
|
+
return function () {
|
|
7895
|
+
cancelHoverClose()
|
|
7896
|
+
cancelHoverOpen()
|
|
7897
|
+
openedByHover = false
|
|
7898
|
+
clearStamp()
|
|
7899
|
+
document.removeEventListener('mouseover', onHeroPointerOver, true)
|
|
7900
|
+
document.removeEventListener('mouseout', onHeroPointerOut, true)
|
|
7901
|
+
delete ui.heroMenu
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
|
|
7905
|
+
/**
|
|
7906
|
+
* The settings page's quick-provider picker — the providers whose models the
|
|
7907
|
+
* model picker's first level carries.
|
|
7908
|
+
*
|
|
7909
|
+
* It is the permission popover's design in multi-select form: the same card
|
|
7910
|
+
* and the same rows (`.dsh-claude-perm-popover` is that shared card, the name
|
|
7911
|
+
* is historical), with the accent check marking the chosen providers. It is a
|
|
7912
|
+
* settings control rather than a takeover, so it opens on click only —
|
|
7913
|
+
* hovering a settings row should not unfold a menu — and it stays open while
|
|
7914
|
+
* rows are toggled.
|
|
7915
|
+
*
|
|
7916
|
+
* The card is appended to <body> rather than rendered inside the settings row:
|
|
7917
|
+
* a dialog clips and may transform its own subtree, so a `position: fixed`
|
|
7918
|
+
* card in there would be positioned against the dialog, not the viewport.
|
|
7919
|
+
*/
|
|
7920
|
+
function installQuickProviders(ctx, ui) {
|
|
7921
|
+
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>'
|
|
7922
|
+
var card = null
|
|
7923
|
+
var cardBody = null
|
|
7924
|
+
var anchor = null
|
|
7925
|
+
var onWrite = null
|
|
7926
|
+
var providers = []
|
|
7927
|
+
var unsubscribe = null
|
|
7928
|
+
|
|
7929
|
+
function close() {
|
|
7930
|
+
if (card !== null && card.parentElement) card.parentElement.removeChild(card)
|
|
7931
|
+
if (anchor !== null) anchor.setAttribute('aria-expanded', 'false')
|
|
7932
|
+
card = null
|
|
7933
|
+
cardBody = null
|
|
7934
|
+
anchor = null
|
|
7935
|
+
onWrite = null
|
|
7936
|
+
document.removeEventListener('pointerdown', onOutsidePointerDown, true)
|
|
7937
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
7938
|
+
}
|
|
7939
|
+
|
|
7940
|
+
function onOutsidePointerDown(e) {
|
|
7941
|
+
if (card === null) return
|
|
7942
|
+
var target = e.target
|
|
7943
|
+
if (card.contains(target)) return
|
|
7944
|
+
if (anchor !== null && anchor.contains(target)) return
|
|
7945
|
+
close()
|
|
7946
|
+
}
|
|
7947
|
+
|
|
7948
|
+
function onKeyDown(e) {
|
|
7949
|
+
if (e.key === 'Escape') close()
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7952
|
+
/** One provider: its name, how many models it offers, and a check when chosen. */
|
|
7953
|
+
function buildRow(provider, chosen, stale) {
|
|
7954
|
+
var row = modelEl('button', 'dsh-claude-popover-item')
|
|
7955
|
+
if (stale) row.classList.add('dsh-claude-model-stale')
|
|
7956
|
+
row.type = 'button'
|
|
7957
|
+
row.setAttribute('role', 'menuitemcheckbox')
|
|
7958
|
+
row.setAttribute('aria-checked', chosen ? 'true' : 'false')
|
|
7959
|
+
row.appendChild(modelEl('span', 'dsh-claude-popover-item-text', provider.name))
|
|
7960
|
+
row.appendChild(modelEl('span', 'dsh-claude-popover-item-badge', stale ? settingsCopy('quickRemoved', 'Removed') : String(provider.count)))
|
|
7961
|
+
var check = modelEl('span', 'dsh-claude-model-check')
|
|
7962
|
+
check.innerHTML = chosen ? CHECK_SVG : ''
|
|
7963
|
+
row.appendChild(check)
|
|
7964
|
+
row.addEventListener('click', function (e) {
|
|
7965
|
+
e.stopPropagation()
|
|
7966
|
+
var next = readPrefs().quickProviders.slice()
|
|
7967
|
+
var at = next.indexOf(provider.id)
|
|
7968
|
+
if (at === -1) next.push(provider.id)
|
|
7969
|
+
else next.splice(at, 1)
|
|
7970
|
+
if (typeof onWrite === 'function') onWrite(next)
|
|
7971
|
+
renderBody()
|
|
7972
|
+
})
|
|
7973
|
+
return row
|
|
7974
|
+
}
|
|
7975
|
+
|
|
7976
|
+
function renderBody() {
|
|
7977
|
+
if (cardBody === null) return
|
|
7978
|
+
while (cardBody.firstChild) cardBody.removeChild(cardBody.firstChild)
|
|
7979
|
+
if (providers.length === 0) {
|
|
7980
|
+
cardBody.appendChild(modelEl('div', 'dsh-claude-model-status', settingsCopy('quickLoading', 'Loading providers…')))
|
|
7981
|
+
return
|
|
7982
|
+
}
|
|
7983
|
+
var chosen = readPrefs().quickProviders
|
|
7984
|
+
// The official service always leads the first level, so it is not a
|
|
7985
|
+
// choice here: it is never listed, and a stored id for it is pruned on
|
|
7986
|
+
// load. A provider that vanished from the catalog still appears, marked
|
|
7987
|
+
// removed, so it can be unchecked out of the stored list instead of
|
|
7988
|
+
// lingering invisibly.
|
|
7989
|
+
var known = {}
|
|
7990
|
+
for (var i = 0; i < providers.length; i++) {
|
|
7991
|
+
if (providers[i].id === MODEL_OFFICIAL_GROUP) continue
|
|
7992
|
+
known[providers[i].id] = true
|
|
7993
|
+
cardBody.appendChild(buildRow(providers[i], chosen.indexOf(providers[i].id) !== -1, false))
|
|
7994
|
+
}
|
|
7995
|
+
for (var s = 0; s < chosen.length; s++) {
|
|
7996
|
+
if (chosen[s] === MODEL_OFFICIAL_GROUP || known[chosen[s]]) continue
|
|
7997
|
+
cardBody.appendChild(buildRow({ id: chosen[s], name: chosen[s], count: 0 }, true, true))
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
|
|
8001
|
+
function open(trigger, write) {
|
|
8002
|
+
anchor = trigger
|
|
8003
|
+
anchor.setAttribute('aria-expanded', 'true')
|
|
8004
|
+
onWrite = write
|
|
8005
|
+
providers = ui.model && typeof ui.model.providers === 'function' ? ui.model.providers() : []
|
|
8006
|
+
card = modelEl('div', 'dsh-claude-perm-popover')
|
|
8007
|
+
card.setAttribute('role', 'menu')
|
|
8008
|
+
card.setAttribute('data-open', 'true')
|
|
8009
|
+
cardBody = modelEl('div', 'dsh-claude-model-popover-body')
|
|
8010
|
+
card.appendChild(cardBody)
|
|
8011
|
+
renderBody()
|
|
8012
|
+
document.body.appendChild(card)
|
|
8013
|
+
positionAnchoredPopover(trigger, card, { side: 'above', gap: 6, important: true })
|
|
8014
|
+
document.addEventListener('pointerdown', onOutsidePointerDown, true)
|
|
8015
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
8016
|
+
}
|
|
8017
|
+
|
|
8018
|
+
function toggle(trigger, write) {
|
|
8019
|
+
if (card === null) open(trigger, write)
|
|
8020
|
+
else close()
|
|
8021
|
+
}
|
|
8022
|
+
|
|
8023
|
+
// The catalog arrives after the settings page may already be open, so an
|
|
8024
|
+
// open card follows it rather than showing an empty list until reopened.
|
|
8025
|
+
if (ui.model && typeof ui.model.onProviders === 'function') {
|
|
8026
|
+
unsubscribe = ui.model.onProviders(function (list) {
|
|
8027
|
+
providers = list
|
|
8028
|
+
if (card !== null) renderBody()
|
|
8029
|
+
})
|
|
8030
|
+
}
|
|
8031
|
+
|
|
8032
|
+
ui.quickProviders = {
|
|
8033
|
+
toggle: toggle,
|
|
8034
|
+
close: close,
|
|
8035
|
+
isOpen: function () { return card !== null },
|
|
8036
|
+
}
|
|
8037
|
+
return function () {
|
|
8038
|
+
close()
|
|
8039
|
+
if (unsubscribe !== null) {
|
|
8040
|
+
try { unsubscribe() } catch (error) { /* already disposed */ }
|
|
8041
|
+
unsubscribe = null
|
|
8042
|
+
}
|
|
8043
|
+
delete ui.quickProviders
|
|
8044
|
+
}
|
|
8045
|
+
}
|
|
8046
|
+
|
|
6110
8047
|
function installAccountFooter(ctx, ui) {
|
|
6111
8048
|
var accountBtn = null
|
|
6112
8049
|
var accountPopover = null
|
|
6113
8050
|
var popoverBody = null
|
|
6114
|
-
var popoverHoverIntent = createHoverIntent(openPopover, closePopover,
|
|
8051
|
+
var popoverHoverIntent = createHoverIntent(openPopover, closePopover, POPOVER_OPEN_DELAY, POPOVER_CLOSE_DELAY)
|
|
6115
8052
|
// Whether the popover is up because it was CLICKED (rather than hovered).
|
|
6116
8053
|
// Clicking the account row opens the ban-screen easter egg and leaves the
|
|
6117
8054
|
// pointer inside the popover, so without this the row's own mouseleave
|
|
@@ -6696,6 +8633,20 @@ window.__ModuleLoader__.load({
|
|
|
6696
8633
|
}
|
|
6697
8634
|
var username = getUsername(ctx)
|
|
6698
8635
|
|
|
8636
|
+
// Idempotent against a torn-down-less reload: client HMR drops the old
|
|
8637
|
+
// fiber's disposals instead of running them, so a previous generation's
|
|
8638
|
+
// button and popover are still in the DOM and this fresh scope knows
|
|
8639
|
+
// nothing of them. Sweep the strays before deciding whether to build —
|
|
8640
|
+
// otherwise the row renders twice (and the orphan keeps a stale name).
|
|
8641
|
+
var strayBtns = footArea.querySelectorAll('.dsh-claude-account-btn')
|
|
8642
|
+
for (var sb = 0; sb < strayBtns.length; sb++) {
|
|
8643
|
+
if (strayBtns[sb] !== accountBtn) strayBtns[sb].parentElement.removeChild(strayBtns[sb])
|
|
8644
|
+
}
|
|
8645
|
+
var strayPops = document.querySelectorAll('body > .dsh-claude-account-popover')
|
|
8646
|
+
for (var sp = 0; sp < strayPops.length; sp++) {
|
|
8647
|
+
if (strayPops[sp] !== accountPopover) strayPops[sp].parentElement.removeChild(strayPops[sp])
|
|
8648
|
+
}
|
|
8649
|
+
|
|
6699
8650
|
if (accountBtn === null || !footArea.contains(accountBtn)) {
|
|
6700
8651
|
if (accountBtn && accountBtn.parentElement) accountBtn.parentElement.removeChild(accountBtn)
|
|
6701
8652
|
accountBtn = document.createElement('div')
|
|
@@ -6714,12 +8665,13 @@ window.__ModuleLoader__.load({
|
|
|
6714
8665
|
// Hover is the default way in; the "Open popovers on hover"
|
|
6715
8666
|
// preference turns it off, leaving the click handler below as the only
|
|
6716
8667
|
// way in (and the only way out, so a click-opened popover does not
|
|
6717
|
-
// vanish when the pointer leaves).
|
|
8668
|
+
// vanish when the pointer leaves). The account row is the one popover
|
|
8669
|
+
// the `account` scope keeps on hover.
|
|
6718
8670
|
accountBtn.addEventListener('mouseenter', function () {
|
|
6719
|
-
if (readPrefs().autoPopover)
|
|
8671
|
+
if (readPrefs().autoPopover !== AUTO_POPOVER_OFF) popoverHoverIntent.scheduleOpen()
|
|
6720
8672
|
})
|
|
6721
8673
|
accountBtn.addEventListener('mouseleave', function () {
|
|
6722
|
-
if (readPrefs().autoPopover && !popoverOpenedByClick) scheduleClosePopover()
|
|
8674
|
+
if (readPrefs().autoPopover !== AUTO_POPOVER_OFF && !popoverOpenedByClick) scheduleClosePopover()
|
|
6723
8675
|
})
|
|
6724
8676
|
accountBtn.addEventListener('click', function (e) {
|
|
6725
8677
|
e.stopPropagation()
|
|
@@ -7279,6 +9231,419 @@ window.__ModuleLoader__.load({
|
|
|
7279
9231
|
}
|
|
7280
9232
|
}
|
|
7281
9233
|
|
|
9234
|
+
/**
|
|
9235
|
+
* The sidebar's workspace section, re-cut as a two-state view.
|
|
9236
|
+
*
|
|
9237
|
+
* 进行中 needs no filtering of its own: the host's tree already leaves
|
|
9238
|
+
* archived sessions out (measured on 0.1.7-alpha.1 — 10 rows, 6 archived, zero
|
|
9239
|
+
* overlap). 已归档 is the skin's own flat list of archived conversations, built
|
|
9240
|
+
* from the archive registry (ids) plus the session list (titles and times),
|
|
9241
|
+
* with a delete button on every row.
|
|
9242
|
+
*
|
|
9243
|
+
* The host DOES ship this filter, but its state lives in a store created per
|
|
9244
|
+
* view instance (`createWorkspaceViewStore`) and is not reachable as a client
|
|
9245
|
+
* service — the only way to use it is to click its options menu, which flashes
|
|
9246
|
+
* a popover in the user's face and still leaves the archived rows buried in
|
|
9247
|
+
* collapsed workspace groups. Listing them here is quieter and flat.
|
|
9248
|
+
*
|
|
9249
|
+
* @param ctx - client context.
|
|
9250
|
+
* @param ui - shared handle table.
|
|
9251
|
+
* @returns teardown.
|
|
9252
|
+
*/
|
|
9253
|
+
function installWorkspaceView(ctx, ui) {
|
|
9254
|
+
/** Trash can for one archived row. */
|
|
9255
|
+
var DELETE_SVG = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2.6 4.2h10.8"/><path d="M6.4 4.2V3a.8.8 0 0 1 .8-.8h1.6a.8.8 0 0 1 .8.8v1.2"/><path d="M4.2 4.2l.6 8.3a1 1 0 0 0 1 .9h4.4a1 1 0 0 0 1-.9l.6-8.3"/><path d="M6.7 6.8v4M9.3 6.8v4"/></svg>'
|
|
9256
|
+
/** Tray with an up arrow: put this conversation back among the live ones. */
|
|
9257
|
+
var RESTORE_SVG = '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2.6 9.2v3.4a1 1 0 0 0 1 1h8.8a1 1 0 0 0 1-1V9.2"/><path d="M8 10.4V2.6"/><path d="M5.2 5.4L8 2.6l2.8 2.8"/></svg>'
|
|
9258
|
+
/**
|
|
9259
|
+
* The host's own Tooltip and icons, reached through the plugin loader's
|
|
9260
|
+
* `require` — the same packages its UI uses, so these row actions look and
|
|
9261
|
+
* behave like the host's. `@deepseek-ai/dsh-client-ui-primitives` exports
|
|
9262
|
+
* `Tooltip`, `IconUnarchiveOutlineRegular` and `IconTrashOutlineRegular`.
|
|
9263
|
+
* Guarded: a loader that hands over nothing leaves the skin's own SVG and a
|
|
9264
|
+
* native title in place.
|
|
9265
|
+
*/
|
|
9266
|
+
var primitives = null
|
|
9267
|
+
var react = null
|
|
9268
|
+
var reactDom = null
|
|
9269
|
+
try { primitives = require('@deepseek-ai/dsh-client-ui-primitives') } catch (error) { primitives = null }
|
|
9270
|
+
try { react = require('react') } catch (error) { react = null }
|
|
9271
|
+
try { reactDom = require('react-dom/client') } catch (error) { reactDom = null }
|
|
9272
|
+
/** React roots holding the row actions, unmounted when the list is rebuilt. */
|
|
9273
|
+
var actionRoots = []
|
|
9274
|
+
var VIEW_ATTR = 'data-dsh-claude-ws-view'
|
|
9275
|
+
var LABEL_ATTR = 'data-dsh-claude-ws-label'
|
|
9276
|
+
var TREE_ATTR = 'data-dsh-claude-ws-tree'
|
|
9277
|
+
var SEGMENTS = [
|
|
9278
|
+
{ id: 'active', key: 'archiveActive', fallback: 'Active' },
|
|
9279
|
+
{ id: 'archived', key: 'archiveArchived', fallback: 'Archived' }
|
|
9280
|
+
]
|
|
9281
|
+
var LABEL_TEXTS = ['工作区', 'Workspace']
|
|
9282
|
+
var view = 'active'
|
|
9283
|
+
var control = null
|
|
9284
|
+
var listHost = null
|
|
9285
|
+
var markedLabel = null
|
|
9286
|
+
var markedTree = null
|
|
9287
|
+
/** `null` until the registry answers; then `[{ id, title, at }]`. */
|
|
9288
|
+
var items = null
|
|
9289
|
+
var loading = false
|
|
9290
|
+
/**
|
|
9291
|
+
* Whether this host archives conversations at all. DSH 0.1.5 has no archive
|
|
9292
|
+
* feature and no `workspaceRegistry` remote, so the section keeps its plain
|
|
9293
|
+
* label there instead of growing a control that can only ever show an empty
|
|
9294
|
+
* list. `null` = not known yet.
|
|
9295
|
+
*/
|
|
9296
|
+
var supported = null
|
|
9297
|
+
var disposed = false
|
|
9298
|
+
var retryTimer = null
|
|
9299
|
+
|
|
9300
|
+
function service(name) {
|
|
9301
|
+
try { return ctx.get(name) } catch (error) { return undefined }
|
|
9302
|
+
}
|
|
9303
|
+
|
|
9304
|
+
function findSection() {
|
|
9305
|
+
var labels = document.querySelectorAll('[class*="sectionLabel"]')
|
|
9306
|
+
var first = null
|
|
9307
|
+
for (var i = 0; i < labels.length; i++) {
|
|
9308
|
+
var text = (labels[i].textContent || '').trim()
|
|
9309
|
+
if (first === null) first = labels[i]
|
|
9310
|
+
for (var t = 0; t < LABEL_TEXTS.length; t++) {
|
|
9311
|
+
if (text === LABEL_TEXTS[t]) return labels[i]
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
return first
|
|
9315
|
+
}
|
|
9316
|
+
|
|
9317
|
+
/**
|
|
9318
|
+
* The tree's rows. `data-row-key` is what 0.1.7 stamps on them, but the
|
|
9319
|
+
* bundled 0.1.5 has no such attribute at all — matching only that made the
|
|
9320
|
+
* whole section bail out on 0.1.5, control and all. The row CLASSES are
|
|
9321
|
+
* stable across both, and the skin's own stylesheet already keys off them.
|
|
9322
|
+
*/
|
|
9323
|
+
var TREE_ROW_SELECTOR = '[data-row-key], [class*="sessionRow"], [class*="projectRow"]'
|
|
9324
|
+
|
|
9325
|
+
function findTree(label) {
|
|
9326
|
+
var region = label.parentElement
|
|
9327
|
+
while (region !== null && region !== document.body) {
|
|
9328
|
+
var tree = region.querySelector('[role="tree"], [class*="list"]')
|
|
9329
|
+
if (tree !== null && tree.querySelector(TREE_ROW_SELECTOR) !== null) return tree
|
|
9330
|
+
region = region.parentElement
|
|
9331
|
+
}
|
|
9332
|
+
return null
|
|
9333
|
+
}
|
|
9334
|
+
|
|
9335
|
+
function relativeTime(at) {
|
|
9336
|
+
if (typeof at !== 'number' || !isFinite(at)) return ''
|
|
9337
|
+
var minutes = Math.floor((Date.now() - at) / 60000)
|
|
9338
|
+
if (minutes < 1) return copyLabel('archiveJustNow', 'Just now')
|
|
9339
|
+
if (minutes < 60) return copyLabel('archiveMinutes', '{count} min', { count: minutes })
|
|
9340
|
+
var hours = Math.floor(minutes / 60)
|
|
9341
|
+
if (hours < 24) return copyLabel('archiveHours', '{count} h', { count: hours })
|
|
9342
|
+
return copyLabel('archiveDays', '{count} d', { count: Math.floor(hours / 24) })
|
|
9343
|
+
}
|
|
9344
|
+
|
|
9345
|
+
/** Ids → `{ title, updatedAt }`, from the session list the shell already has. */
|
|
9346
|
+
function titlesFor() {
|
|
9347
|
+
var sessions = service('remote.session')
|
|
9348
|
+
if (sessions === undefined || sessions === null || typeof sessions.list !== 'function') return Promise.resolve({})
|
|
9349
|
+
return sessions.list({}).then(function (result) {
|
|
9350
|
+
var map = {}
|
|
9351
|
+
var rows = (result && result.ok === true && result.value && result.value.items) || []
|
|
9352
|
+
for (var i = 0; i < rows.length; i++) {
|
|
9353
|
+
var values = rows[i].projections && rows[i].projections.values
|
|
9354
|
+
map[rows[i].sessionId] = { title: values ? values.title : null, updatedAt: rows[i].updatedAt }
|
|
9355
|
+
}
|
|
9356
|
+
return map
|
|
9357
|
+
}).catch(function () { return {} })
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
function loadArchived(attempt) {
|
|
9361
|
+
function retry() {
|
|
9362
|
+
if (disposed || attempt >= 4) {
|
|
9363
|
+
// No archive service answered: this host does not have one (0.1.5), so
|
|
9364
|
+
// the section goes back to being a plain label.
|
|
9365
|
+
supported = false
|
|
9366
|
+
items = []
|
|
9367
|
+
loading = false
|
|
9368
|
+
renderList()
|
|
9369
|
+
return
|
|
9370
|
+
}
|
|
9371
|
+
loading = true
|
|
9372
|
+
renderList()
|
|
9373
|
+
retryTimer = setTimeout(function () {
|
|
9374
|
+
retryTimer = null
|
|
9375
|
+
loadArchived(attempt + 1)
|
|
9376
|
+
}, 600)
|
|
9377
|
+
}
|
|
9378
|
+
var registry = service('remote.workspaceRegistry')
|
|
9379
|
+
if (registry === undefined || registry === null || typeof registry.archivedSessionMetadata !== 'function') {
|
|
9380
|
+
retry()
|
|
9381
|
+
return
|
|
9382
|
+
}
|
|
9383
|
+
loading = true
|
|
9384
|
+
renderList()
|
|
9385
|
+
registry.archivedSessionMetadata().then(function (result) {
|
|
9386
|
+
if (!result || result.ok !== true) throw new Error('archived metadata unavailable')
|
|
9387
|
+
var rows = (result.value && result.value.items) || []
|
|
9388
|
+
supported = true
|
|
9389
|
+
items = rows.map(function (row) { return { id: row.sessionId, title: null, at: row.createdAt } })
|
|
9390
|
+
items.sort(function (a, b) { return (b.at || 0) - (a.at || 0) })
|
|
9391
|
+
loading = false
|
|
9392
|
+
renderList()
|
|
9393
|
+
return fillTitles(0)
|
|
9394
|
+
}).catch(function () { retry() })
|
|
9395
|
+
}
|
|
9396
|
+
|
|
9397
|
+
/**
|
|
9398
|
+
* Titles and times ride the session list, which the shell may still be
|
|
9399
|
+
* loading when the registry answers — a row falls back to its id until they
|
|
9400
|
+
* land, and a couple of re-reads fill them in. Bounded, and abandoned the
|
|
9401
|
+
* moment the view is left or the fragment is torn down.
|
|
9402
|
+
*/
|
|
9403
|
+
function fillTitles(attempt) {
|
|
9404
|
+
return titlesFor().then(function (titles) {
|
|
9405
|
+
if (items === null) return
|
|
9406
|
+
var missing = false
|
|
9407
|
+
for (var i = 0; i < items.length; i++) {
|
|
9408
|
+
var meta = titles[items[i].id]
|
|
9409
|
+
if (meta && meta.title) items[i].title = meta.title
|
|
9410
|
+
if (meta && meta.updatedAt) items[i].at = meta.updatedAt
|
|
9411
|
+
if (!items[i].title) missing = true
|
|
9412
|
+
}
|
|
9413
|
+
items.sort(function (a, b) { return (b.at || 0) - (a.at || 0) })
|
|
9414
|
+
renderList()
|
|
9415
|
+
if (!missing || attempt >= 4 || view !== 'archived' || disposed) return
|
|
9416
|
+
return new Promise(function (resolve) {
|
|
9417
|
+
retryTimer = setTimeout(resolve, 500)
|
|
9418
|
+
}).then(function () {
|
|
9419
|
+
retryTimer = null
|
|
9420
|
+
if (disposed || view !== 'archived') return
|
|
9421
|
+
return fillTitles(attempt + 1)
|
|
9422
|
+
})
|
|
9423
|
+
})
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
function openArchived(id) {
|
|
9427
|
+
var sessions = service('sessions')
|
|
9428
|
+
if (sessions !== undefined && sessions !== null && typeof sessions.open === 'function') sessions.open(id)
|
|
9429
|
+
}
|
|
9430
|
+
|
|
9431
|
+
function removeArchived(id) {
|
|
9432
|
+
var registry = service('remote.workspaceRegistry')
|
|
9433
|
+
if (registry === undefined || registry === null || typeof registry.deleteSession !== 'function') return
|
|
9434
|
+
registry.deleteSession(id).then(function (result) {
|
|
9435
|
+
if (!result || result.ok !== true) return
|
|
9436
|
+
if (items !== null) items = items.filter(function (row) { return row.id !== id })
|
|
9437
|
+
renderList()
|
|
9438
|
+
}).catch(function () { /* the row stays; the next read tells the truth */ })
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
/** Put a conversation back among the live ones; it leaves this list. */
|
|
9442
|
+
function restoreArchived(id) {
|
|
9443
|
+
var registry = service('remote.workspaceRegistry')
|
|
9444
|
+
if (registry === undefined || registry === null || typeof registry.unarchiveSession !== 'function') return
|
|
9445
|
+
registry.unarchiveSession(id).then(function (result) {
|
|
9446
|
+
if (!result || result.ok !== true) return
|
|
9447
|
+
if (items !== null) items = items.filter(function (row) { return row.id !== id })
|
|
9448
|
+
renderList()
|
|
9449
|
+
// The host's own tree has to be told to pick the conversation back up.
|
|
9450
|
+
var sessions = service('sessions')
|
|
9451
|
+
if (sessions !== undefined && sessions !== null && typeof sessions.refresh === 'function') sessions.refresh()
|
|
9452
|
+
}).catch(function () { /* the row stays; the next read tells the truth */ })
|
|
9453
|
+
}
|
|
9454
|
+
|
|
9455
|
+
/**
|
|
9456
|
+
* One row action: the host's icon inside the host's tooltip when the loader
|
|
9457
|
+
* gives us both, and the skin's own SVG plus a native title when it does not.
|
|
9458
|
+
*/
|
|
9459
|
+
function actionButton(kind, label, fallbackSvg, onClick) {
|
|
9460
|
+
var wrapper = modelEl('span', 'dsh-claude-archive-action')
|
|
9461
|
+
var className = kind === 'restore' ? 'dsh-claude-archive-restore' : 'dsh-claude-archive-delete'
|
|
9462
|
+
var Icon = primitives === null ? null : (kind === 'restore' ? primitives.IconUnarchiveOutlineRegular : primitives.IconTrashOutlineRegular)
|
|
9463
|
+
if (react !== null && reactDom !== null && primitives !== null && primitives.Tooltip && Icon) {
|
|
9464
|
+
var root = reactDom.createRoot(wrapper)
|
|
9465
|
+
actionRoots.push(root)
|
|
9466
|
+
root.render(react.createElement(primitives.Tooltip, { label: label, side: 'top', delayMs: 500 },
|
|
9467
|
+
react.createElement('button', { type: 'button', className: className, 'aria-label': label, title: label, onClick: onClick },
|
|
9468
|
+
react.createElement(Icon, { size: 14 }))))
|
|
9469
|
+
return wrapper
|
|
9470
|
+
}
|
|
9471
|
+
var button = modelEl('button', className)
|
|
9472
|
+
button.type = 'button'
|
|
9473
|
+
button.setAttribute('aria-label', label)
|
|
9474
|
+
button.setAttribute('title', label)
|
|
9475
|
+
button.innerHTML = fallbackSvg
|
|
9476
|
+
button.addEventListener('click', onClick)
|
|
9477
|
+
wrapper.appendChild(button)
|
|
9478
|
+
return wrapper
|
|
9479
|
+
}
|
|
9480
|
+
|
|
9481
|
+
function buildArchivedRow(item) {
|
|
9482
|
+
var row = modelEl('div', 'dsh-claude-archive-row')
|
|
9483
|
+
row.setAttribute('role', 'button')
|
|
9484
|
+
row.setAttribute('tabindex', '0')
|
|
9485
|
+
row.setAttribute('data-session-id', item.id)
|
|
9486
|
+
row.appendChild(modelEl('span', 'dsh-claude-archive-title', item.title || copyLabel('archiveUntitled', 'Untitled conversation')))
|
|
9487
|
+
row.appendChild(modelEl('span', 'dsh-claude-archive-time', relativeTime(item.at)))
|
|
9488
|
+
// The host's archived rows offer an unarchive action; the skin's list
|
|
9489
|
+
// carries the same pair, so leaving the archived view is not the only way
|
|
9490
|
+
// back to a conversation.
|
|
9491
|
+
row.appendChild(actionButton('restore', copyLabel('archiveRestore', 'Unarchive conversation'), RESTORE_SVG, function (event) {
|
|
9492
|
+
event.stopPropagation()
|
|
9493
|
+
restoreArchived(item.id)
|
|
9494
|
+
}))
|
|
9495
|
+
row.appendChild(actionButton('delete', copyLabel('archiveDelete', 'Delete conversation'), DELETE_SVG, function (event) {
|
|
9496
|
+
event.stopPropagation()
|
|
9497
|
+
removeArchived(item.id)
|
|
9498
|
+
}))
|
|
9499
|
+
row.addEventListener('click', function () { openArchived(item.id) })
|
|
9500
|
+
return row
|
|
9501
|
+
}
|
|
9502
|
+
|
|
9503
|
+
function renderList() {
|
|
9504
|
+
if (listHost === null) return
|
|
9505
|
+
// The actions live in React roots; drop them before the rows go, or every
|
|
9506
|
+
// rebuild would leave a tree behind.
|
|
9507
|
+
for (var r = 0; r < actionRoots.length; r++) {
|
|
9508
|
+
try { actionRoots[r].unmount() } catch (error) { /* already gone */ }
|
|
9509
|
+
}
|
|
9510
|
+
actionRoots = []
|
|
9511
|
+
while (listHost.firstChild) listHost.removeChild(listHost.firstChild)
|
|
9512
|
+
if (loading) {
|
|
9513
|
+
listHost.appendChild(modelEl('div', 'dsh-claude-archive-status', copyLabel('archiveLoading', 'Loading…')))
|
|
9514
|
+
return
|
|
9515
|
+
}
|
|
9516
|
+
if (items === null) return
|
|
9517
|
+
if (items.length === 0) {
|
|
9518
|
+
listHost.appendChild(modelEl('div', 'dsh-claude-archive-status', copyLabel('archiveEmpty', 'No archived conversations')))
|
|
9519
|
+
return
|
|
9520
|
+
}
|
|
9521
|
+
for (var i = 0; i < items.length; i++) listHost.appendChild(buildArchivedRow(items[i]))
|
|
9522
|
+
}
|
|
9523
|
+
|
|
9524
|
+
function buildControl() {
|
|
9525
|
+
var group = modelEl('div', 'dsh-claude-ws-segments')
|
|
9526
|
+
group.setAttribute('role', 'radiogroup')
|
|
9527
|
+
for (var i = 0; i < SEGMENTS.length; i++) {
|
|
9528
|
+
var item = modelEl('button', 'dsh-claude-ws-segment', '')
|
|
9529
|
+
item.type = 'button'
|
|
9530
|
+
item.setAttribute('role', 'radio')
|
|
9531
|
+
item.setAttribute('data-view', SEGMENTS[i].id)
|
|
9532
|
+
group.appendChild(item)
|
|
9533
|
+
}
|
|
9534
|
+
group.addEventListener('click', function (event) {
|
|
9535
|
+
var target = event.target
|
|
9536
|
+
var item = target !== null && typeof target.closest === 'function' ? target.closest('.dsh-claude-ws-segment') : null
|
|
9537
|
+
if (item === null) return
|
|
9538
|
+
event.stopPropagation()
|
|
9539
|
+
event.preventDefault()
|
|
9540
|
+
setView(item.getAttribute('data-view'))
|
|
9541
|
+
})
|
|
9542
|
+
return group
|
|
9543
|
+
}
|
|
9544
|
+
|
|
9545
|
+
function setView(next) {
|
|
9546
|
+
if (next !== 'active' && next !== 'archived') return
|
|
9547
|
+
if (next === view) return
|
|
9548
|
+
view = next
|
|
9549
|
+
if (view === 'archived' && items === null && !loading) loadArchived(0)
|
|
9550
|
+
sync()
|
|
9551
|
+
}
|
|
9552
|
+
|
|
9553
|
+
function sync() {
|
|
9554
|
+
var label = findSection()
|
|
9555
|
+
if (label === null || label.parentElement === null) return
|
|
9556
|
+
var header = label.parentElement
|
|
9557
|
+
// Find out whether this host archives at all, once, on the first pass —
|
|
9558
|
+
// the answer decides whether the section keeps its label.
|
|
9559
|
+
if (supported === null && !loading) loadArchived(0)
|
|
9560
|
+
if (supported !== true) {
|
|
9561
|
+
if (control !== null) {
|
|
9562
|
+
if (control.parentElement !== null) control.parentElement.removeChild(control)
|
|
9563
|
+
control = null
|
|
9564
|
+
}
|
|
9565
|
+
if (listHost !== null) {
|
|
9566
|
+
if (listHost.parentElement !== null) listHost.parentElement.removeChild(listHost)
|
|
9567
|
+
listHost = null
|
|
9568
|
+
}
|
|
9569
|
+
if (markedLabel !== null) {
|
|
9570
|
+
markedLabel.removeAttribute(LABEL_ATTR)
|
|
9571
|
+
markedLabel = null
|
|
9572
|
+
}
|
|
9573
|
+
return
|
|
9574
|
+
}
|
|
9575
|
+
if (markedLabel !== label) {
|
|
9576
|
+
if (markedLabel !== null) markedLabel.removeAttribute(LABEL_ATTR)
|
|
9577
|
+
markedLabel = label
|
|
9578
|
+
label.setAttribute(LABEL_ATTR, '')
|
|
9579
|
+
}
|
|
9580
|
+
if (control === null || control.parentElement !== header) {
|
|
9581
|
+
if (control !== null && control.parentElement !== null) control.parentElement.removeChild(control)
|
|
9582
|
+
control = buildControl()
|
|
9583
|
+
header.insertBefore(control, header.firstChild)
|
|
9584
|
+
}
|
|
9585
|
+
for (var i = 0; i < control.children.length; i++) {
|
|
9586
|
+
var item = control.children[i]
|
|
9587
|
+
var id = item.getAttribute('data-view')
|
|
9588
|
+
for (var s = 0; s < SEGMENTS.length; s++) {
|
|
9589
|
+
if (SEGMENTS[s].id !== id) continue
|
|
9590
|
+
var text = copyLabel(SEGMENTS[s].key, SEGMENTS[s].fallback)
|
|
9591
|
+
if (item.textContent !== text) item.textContent = text
|
|
9592
|
+
}
|
|
9593
|
+
var on = id === view
|
|
9594
|
+
if (item.getAttribute('aria-checked') !== (on ? 'true' : 'false')) item.setAttribute('aria-checked', on ? 'true' : 'false')
|
|
9595
|
+
}
|
|
9596
|
+
var tree = findTree(label)
|
|
9597
|
+
if (tree === null) return
|
|
9598
|
+
if (markedTree !== tree) {
|
|
9599
|
+
if (markedTree !== null) markedTree.removeAttribute(TREE_ATTR)
|
|
9600
|
+
markedTree = tree
|
|
9601
|
+
tree.setAttribute(TREE_ATTR, '')
|
|
9602
|
+
}
|
|
9603
|
+
var host = tree.parentElement
|
|
9604
|
+
if (host === null) return
|
|
9605
|
+
if (listHost === null || listHost.parentElement !== host) {
|
|
9606
|
+
if (listHost !== null && listHost.parentElement !== null) listHost.parentElement.removeChild(listHost)
|
|
9607
|
+
listHost = modelEl('div', 'dsh-claude-archive-list')
|
|
9608
|
+
host.insertBefore(listHost, tree.nextSibling)
|
|
9609
|
+
renderList()
|
|
9610
|
+
}
|
|
9611
|
+
// The sidebar re-renders its own way: when React replaces the container
|
|
9612
|
+
// that held the list, the old copy can stay in the document while a new
|
|
9613
|
+
// one is built elsewhere. Sweep every copy but the live one, the same way
|
|
9614
|
+
// the stats popover sweeps its strays.
|
|
9615
|
+
var strays = document.querySelectorAll('.dsh-claude-archive-list')
|
|
9616
|
+
for (var k = 0; k < strays.length; k++) {
|
|
9617
|
+
if (strays[k] === listHost) continue
|
|
9618
|
+
if (strays[k].parentElement !== null) strays[k].parentElement.removeChild(strays[k])
|
|
9619
|
+
}
|
|
9620
|
+
if (host.getAttribute(VIEW_ATTR) !== view) host.setAttribute(VIEW_ATTR, view)
|
|
9621
|
+
}
|
|
9622
|
+
|
|
9623
|
+
ui.workspace = { sync: sync }
|
|
9624
|
+
|
|
9625
|
+
return function () {
|
|
9626
|
+
disposed = true
|
|
9627
|
+
for (var r = 0; r < actionRoots.length; r++) {
|
|
9628
|
+
try { actionRoots[r].unmount() } catch (error) { /* already gone */ }
|
|
9629
|
+
}
|
|
9630
|
+
actionRoots = []
|
|
9631
|
+
if (retryTimer !== null) {
|
|
9632
|
+
clearTimeout(retryTimer)
|
|
9633
|
+
retryTimer = null
|
|
9634
|
+
}
|
|
9635
|
+
if (control !== null && control.parentElement !== null) control.parentElement.removeChild(control)
|
|
9636
|
+
if (listHost !== null && listHost.parentElement !== null) listHost.parentElement.removeChild(listHost)
|
|
9637
|
+
if (markedLabel !== null) markedLabel.removeAttribute(LABEL_ATTR)
|
|
9638
|
+
if (markedTree !== null) markedTree.removeAttribute(TREE_ATTR)
|
|
9639
|
+
control = null
|
|
9640
|
+
listHost = null
|
|
9641
|
+
markedLabel = null
|
|
9642
|
+
markedTree = null
|
|
9643
|
+
delete ui.workspace
|
|
9644
|
+
}
|
|
9645
|
+
}
|
|
9646
|
+
|
|
7282
9647
|
function installScheduler(ctx, ui) {
|
|
7283
9648
|
function onGlobalPointerDown(e) {
|
|
7284
9649
|
var target = e.target
|
|
@@ -7339,6 +9704,28 @@ window.__ModuleLoader__.load({
|
|
|
7339
9704
|
}
|
|
7340
9705
|
}
|
|
7341
9706
|
|
|
9707
|
+
// Focus moving into the composer means the user is about to type: every
|
|
9708
|
+
// popover the skin keeps open around the card is in the way there, so all
|
|
9709
|
+
// of them close. `focusin` bubbles (unlike focus), so one listener covers
|
|
9710
|
+
// the card and everything inside it; the observer's attributeFilter does
|
|
9711
|
+
// not watch focus events, so this cannot feed itself another pass.
|
|
9712
|
+
function onComposerFocusIn(e) {
|
|
9713
|
+
var target = e.target
|
|
9714
|
+
if (!target || typeof target.closest !== 'function') return
|
|
9715
|
+
if (target.closest('[data-composer-card]') === null) return
|
|
9716
|
+
if (ui.model) ui.model.close()
|
|
9717
|
+
if (ui.permissions) ui.permissions.closeMenu()
|
|
9718
|
+
if (ui.permissions && ui.permissions.closeStats) ui.permissions.closeStats()
|
|
9719
|
+
if (ui.footer) ui.footer.close()
|
|
9720
|
+
// The hero menu has no close of its own — it lives and dies with the
|
|
9721
|
+
// host's hover state, and syncHeroMenu notices when it is gone — so the
|
|
9722
|
+
// call is guarded like closeStats above. Unguarded it threw a TypeError
|
|
9723
|
+
// on every composer focus-in, which also aborted the rest of this
|
|
9724
|
+
// handler for that event.
|
|
9725
|
+
if (ui.heroMenu && ui.heroMenu.close) ui.heroMenu.close()
|
|
9726
|
+
if (ui.quickProviders) ui.quickProviders.close()
|
|
9727
|
+
}
|
|
9728
|
+
|
|
7342
9729
|
function onComposerInput(e) {
|
|
7343
9730
|
var target = e.target
|
|
7344
9731
|
if (!target) return
|
|
@@ -7352,12 +9739,14 @@ window.__ModuleLoader__.load({
|
|
|
7352
9739
|
document.addEventListener('keydown', onGlobalKeyDown, true)
|
|
7353
9740
|
document.addEventListener('input', onComposerInput, true)
|
|
7354
9741
|
document.addEventListener('compositionend', onComposerInput, true)
|
|
9742
|
+
document.addEventListener('focusin', onComposerFocusIn, true)
|
|
7355
9743
|
|
|
7356
9744
|
// Both fixed popovers are anchored to their trigger; scroll of the page
|
|
7357
9745
|
// (not the conversation's own auto-stick) and resizes move the anchor, so
|
|
7358
9746
|
// whichever is open must re-resolve it.
|
|
7359
9747
|
function onFixedPopoverViewportChange() {
|
|
7360
9748
|
if (ui.model) ui.model.reposition()
|
|
9749
|
+
if (ui.heroMenu) ui.heroMenu.reposition()
|
|
7361
9750
|
if (ui.footer && ui.footer.isOpen()) ui.footer.reposition()
|
|
7362
9751
|
}
|
|
7363
9752
|
window.addEventListener('resize', onFixedPopoverViewportChange)
|
|
@@ -7429,7 +9818,9 @@ window.__ModuleLoader__.load({
|
|
|
7429
9818
|
if (ui.copy) ui.copy.sync()
|
|
7430
9819
|
if (ui.permissions) ui.permissions.sync()
|
|
7431
9820
|
if (ui.model) ui.model.sync()
|
|
9821
|
+
if (ui.heroMenu) ui.heroMenu.sync()
|
|
7432
9822
|
if (ui.footer) ui.footer.sync()
|
|
9823
|
+
if (ui.workspace) ui.workspace.sync()
|
|
7433
9824
|
if (ui.settings) ui.settings.sync()
|
|
7434
9825
|
// Covers the rail toggle (and any reflow) while the popover is open:
|
|
7435
9826
|
// its anchor moved without a window resize or a page scroll.
|
|
@@ -7497,6 +9888,7 @@ window.__ModuleLoader__.load({
|
|
|
7497
9888
|
document.removeEventListener('keydown', onGlobalKeyDown, true)
|
|
7498
9889
|
document.removeEventListener('input', onComposerInput, true)
|
|
7499
9890
|
document.removeEventListener('compositionend', onComposerInput, true)
|
|
9891
|
+
document.removeEventListener('focusin', onComposerFocusIn, true)
|
|
7500
9892
|
// Safety-net DOM sweep. Feature teardowns run after this and tolerate
|
|
7501
9893
|
// nodes already being detached.
|
|
7502
9894
|
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]')
|
|
@@ -7516,11 +9908,15 @@ window.__ModuleLoader__.load({
|
|
|
7516
9908
|
}
|
|
7517
9909
|
|
|
7518
9910
|
/**
|
|
7519
|
-
* The settings page
|
|
7520
|
-
*
|
|
7521
|
-
*
|
|
7522
|
-
*
|
|
7523
|
-
*
|
|
9911
|
+
* The skin's settings page, mounted by whichever seat this host has.
|
|
9912
|
+
*
|
|
9913
|
+
* 0.1.7 moved a bundle's own configuration onto its plugin page, so there
|
|
9914
|
+
* the rows register as a `plugins.bundle.config` entry keyed by the package
|
|
9915
|
+
* name. Older hosts have no such slot and keep the full-page
|
|
9916
|
+
* `settings.section` entry. Both render the same component, and neither
|
|
9917
|
+
* seat hands it a store: it reads and writes the skin preferences through
|
|
9918
|
+
* src/context/prefs.js, which owns the host round trip, and follows changes
|
|
9919
|
+
* the same way the rest of the skin does.
|
|
7524
9920
|
*
|
|
7525
9921
|
* Copy comes from the model copy document's `settings` block, so the page
|
|
7526
9922
|
* follows the shell language like every other string the skin paints. The
|
|
@@ -7530,7 +9926,14 @@ window.__ModuleLoader__.load({
|
|
|
7530
9926
|
* `.dsh-claude-segment` classes — the same control the composer's permission
|
|
7531
9927
|
* picker uses — so the two read as one design instead of two lookalikes.
|
|
7532
9928
|
*/
|
|
7533
|
-
|
|
9929
|
+
/**
|
|
9930
|
+
* The quick-provider popover (src/overrides/quick-providers.js). The settings
|
|
9931
|
+
* row is React-rendered while that card is imperative, so the row reaches the
|
|
9932
|
+
* popover through this handle.
|
|
9933
|
+
*/
|
|
9934
|
+
var quickProviderApi = null
|
|
9935
|
+
|
|
9936
|
+
function ClaudeStyleSettingsSection(props) {
|
|
7534
9937
|
var state = React.useState(readPrefs())
|
|
7535
9938
|
var prefs = state[0]
|
|
7536
9939
|
var setPrefs = state[1]
|
|
@@ -7541,6 +9944,7 @@ window.__ModuleLoader__.load({
|
|
|
7541
9944
|
var username = usernameState[0]
|
|
7542
9945
|
var setUsername = usernameState[1]
|
|
7543
9946
|
var usernameTimer = React.useRef(null)
|
|
9947
|
+
var quickTrigger = React.useRef(null)
|
|
7544
9948
|
|
|
7545
9949
|
// The skin's own apply-side writes land here too (a reload, a conflict
|
|
7546
9950
|
// re-read), so the page never drifts from what the document says.
|
|
@@ -7669,6 +10073,17 @@ window.__ModuleLoader__.load({
|
|
|
7669
10073
|
{ value: BAN_LOCALE_ZH, label: settingsCopy('banLocaleZh', '中文') },
|
|
7670
10074
|
{ value: BAN_LOCALE_EN, label: settingsCopy('banLocaleEn', 'English') },
|
|
7671
10075
|
]
|
|
10076
|
+
var autoPopoverOptions = [
|
|
10077
|
+
{ value: AUTO_POPOVER_OFF, label: settingsCopy('autoPopoverOff', 'Off') },
|
|
10078
|
+
{ value: AUTO_POPOVER_ACCOUNT, label: settingsCopy('autoPopoverAccount', 'Account only') },
|
|
10079
|
+
{ value: AUTO_POPOVER_ALL, label: settingsCopy('autoPopoverAll', 'All') },
|
|
10080
|
+
]
|
|
10081
|
+
|
|
10082
|
+
/** What the quick-provider trigger reads: how many, or nothing chosen. */
|
|
10083
|
+
var quickSummary = function (chosen) {
|
|
10084
|
+
if (chosen.length === 0) return settingsCopy('quickNone', 'None')
|
|
10085
|
+
return settingsCopy('quickCount', '{count} providers', { count: chosen.length })
|
|
10086
|
+
}
|
|
7672
10087
|
|
|
7673
10088
|
var rows = [
|
|
7674
10089
|
row(
|
|
@@ -7715,8 +10130,8 @@ window.__ModuleLoader__.load({
|
|
|
7715
10130
|
row(
|
|
7716
10131
|
'autoPopover',
|
|
7717
10132
|
settingsCopy('autoPopoverTitle', 'Open popovers on hover'),
|
|
7718
|
-
settingsCopy('autoPopoverDesc', '
|
|
7719
|
-
|
|
10133
|
+
settingsCopy('autoPopoverDesc', 'Which popovers hover opens. Off leaves them all click-to-open.'),
|
|
10134
|
+
segment(autoPopoverOptions, prefs.autoPopover, function (value) { write({ autoPopover: value }) }),
|
|
7720
10135
|
),
|
|
7721
10136
|
row(
|
|
7722
10137
|
'composerScope',
|
|
@@ -7724,6 +10139,28 @@ window.__ModuleLoader__.load({
|
|
|
7724
10139
|
settingsCopy('composerDesc', 'Which input area the skin restyles: the new-conversation page, the conversation, or both.'),
|
|
7725
10140
|
segment(scopeOptions, prefs.composerScope, function (value) { write({ composerScope: value }) }),
|
|
7726
10141
|
),
|
|
10142
|
+
row(
|
|
10143
|
+
'modelPicker',
|
|
10144
|
+
settingsCopy('pickerTitle', 'Redraw the model picker'),
|
|
10145
|
+
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.'),
|
|
10146
|
+
toggle(prefs.modelPicker, function (value) { write({ modelPicker: value }) }),
|
|
10147
|
+
),
|
|
10148
|
+
row(
|
|
10149
|
+
'quickProviders',
|
|
10150
|
+
settingsCopy('quickTitle', 'Quick providers'),
|
|
10151
|
+
settingsCopy('quickDesc', 'Providers whose models the picker\'s first level lists, one rule between providers. Nothing picked keeps the default: the official service.'),
|
|
10152
|
+
React.createElement('button', {
|
|
10153
|
+
type: 'button',
|
|
10154
|
+
ref: quickTrigger,
|
|
10155
|
+
className: 'dsh-claude-settings-picker',
|
|
10156
|
+
'aria-haspopup': 'menu',
|
|
10157
|
+
'aria-expanded': 'false',
|
|
10158
|
+
onClick: function () {
|
|
10159
|
+
if (quickProviderApi === null || quickTrigger.current === null) return
|
|
10160
|
+
quickProviderApi.toggle(quickTrigger.current, function (next) { write({ quickProviders: next }) })
|
|
10161
|
+
},
|
|
10162
|
+
}, quickSummary(prefs.quickProviders)),
|
|
10163
|
+
),
|
|
7727
10164
|
row(
|
|
7728
10165
|
'banLocale',
|
|
7729
10166
|
settingsCopy('banLocaleTitle', 'Account-hold easter egg language'),
|
|
@@ -7736,14 +10173,35 @@ window.__ModuleLoader__.load({
|
|
|
7736
10173
|
rows.push(React.createElement('div', { className: 'dsh-claude-settings-error', key: 'error' }, error))
|
|
7737
10174
|
}
|
|
7738
10175
|
|
|
10176
|
+
// The plugin page already heads the form with the bundle's own title and
|
|
10177
|
+
// description, so the embedded rendering drops the skin's title rather
|
|
10178
|
+
// than printing it twice.
|
|
10179
|
+
var embedded = !!(props && props.embed)
|
|
7739
10180
|
return React.createElement(
|
|
7740
10181
|
'div',
|
|
7741
|
-
{ className: 'dsh-claude-settings' },
|
|
7742
|
-
React.createElement('div', { className: 'dsh-claude-settings-title' }, settingsCopy('title', 'Claude Style')),
|
|
10182
|
+
{ className: embedded ? 'dsh-claude-settings dsh-claude-settings-embedded' : 'dsh-claude-settings' },
|
|
10183
|
+
embedded ? null : React.createElement('div', { className: 'dsh-claude-settings-title' }, settingsCopy('title', 'Claude Style')),
|
|
7743
10184
|
rows,
|
|
7744
10185
|
)
|
|
7745
10186
|
}
|
|
7746
10187
|
|
|
10188
|
+
/**
|
|
10189
|
+
* The plugin page's configuration entry (0.1.7+).
|
|
10190
|
+
*
|
|
10191
|
+
* A `plugins.bundle.config` entry is asked for two views: `summary` is the
|
|
10192
|
+
* one-liner on the bundle's card, `page` is the form itself. The host does
|
|
10193
|
+
* not hand this seat a form — the entry is keyed to the package, not to a
|
|
10194
|
+
* namespace — so the rows read and write through src/context/prefs.js like
|
|
10195
|
+
* every other surface in the skin, which is where the official form is
|
|
10196
|
+
* bound.
|
|
10197
|
+
*/
|
|
10198
|
+
function ClaudeStyleBundleConfig(props) {
|
|
10199
|
+
if (props && props.view === 'summary') {
|
|
10200
|
+
return React.createElement('span', null, settingsCopy('title', 'Claude Style'))
|
|
10201
|
+
}
|
|
10202
|
+
return React.createElement(ClaudeStyleSettingsSection, { embed: true })
|
|
10203
|
+
}
|
|
10204
|
+
|
|
7747
10205
|
/**
|
|
7748
10206
|
* Register the settings section.
|
|
7749
10207
|
*
|
|
@@ -7780,20 +10238,49 @@ window.__ModuleLoader__.load({
|
|
|
7780
10238
|
|
|
7781
10239
|
function installSettingsSection(ctx, ui) {
|
|
7782
10240
|
loadModelCopy()
|
|
10241
|
+
// The settings services are up by now even when they were not at apply
|
|
10242
|
+
// time, so retry the official-form binding before choosing a seat. It is
|
|
10243
|
+
// idempotent: a host without `configForms` keeps the plugin's own route.
|
|
10244
|
+
adoptSettingsForm(ctx)
|
|
7783
10245
|
if (ui) {
|
|
7784
10246
|
ui.settings = {
|
|
7785
10247
|
sync: syncSettingsNav,
|
|
7786
10248
|
}
|
|
10249
|
+
quickProviderApi = ui.quickProviders || null
|
|
7787
10250
|
}
|
|
7788
10251
|
if (typeof ctx.inject !== 'function') return function () {}
|
|
7789
10252
|
var fiber = ctx.inject(['slots'], function (scope) {
|
|
7790
10253
|
var slots = scope.get('slots')
|
|
7791
10254
|
if (slots === void 0 || slots === null || typeof slots.inject !== 'function') return
|
|
10255
|
+
// 0.1.7 keeps a bundle's own configuration on the plugin's page: the
|
|
10256
|
+
// entry is keyed by the bundle's package name and rendered there —
|
|
10257
|
+
// `view: 'page'` for the form, `view: 'summary'` for the card's
|
|
10258
|
+
// one-liner. The slot is declared by that host's plugin manager, so this
|
|
10259
|
+
// registration is a no-op on an older host.
|
|
10260
|
+
scope.effect(function () {
|
|
10261
|
+
return slots.inject(BUNDLE_CONFIG_SLOT, function () {
|
|
10262
|
+
return slots.register(
|
|
10263
|
+
{
|
|
10264
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
10265
|
+
key: PACKAGE_NAME,
|
|
10266
|
+
label: function () { return settingsCopy('title', 'Claude Style') },
|
|
10267
|
+
},
|
|
10268
|
+
ClaudeStyleBundleConfig,
|
|
10269
|
+
)
|
|
10270
|
+
})
|
|
10271
|
+
}, 'dsh-claude-style: plugin page')
|
|
10272
|
+
// The full-page section is the older host's seat. 0.1.7 still declares
|
|
10273
|
+
// the slot, but there the skin's settings live on its plugin page — and
|
|
10274
|
+
// a host that serves `configForms` is by definition the newer one, so
|
|
10275
|
+
// that check tells the two apart without a version probe. (The settings
|
|
10276
|
+
// shell cannot have declared this slot before `configForms` mounted: it
|
|
10277
|
+
// injects the service itself.)
|
|
7792
10278
|
scope.effect(function () {
|
|
7793
|
-
return slots.inject(
|
|
10279
|
+
return slots.inject(SETTINGS_SECTION_SLOT, function () {
|
|
10280
|
+
if (hostConfigForms(ctx) !== null) return function () {}
|
|
7794
10281
|
return slots.register(
|
|
7795
10282
|
{
|
|
7796
|
-
name:
|
|
10283
|
+
name: SETTINGS_SECTION_SLOT,
|
|
7797
10284
|
id: 'claude-style',
|
|
7798
10285
|
order: 22,
|
|
7799
10286
|
// A function, so the navigation entry localizes once the copy
|
|
@@ -7809,6 +10296,7 @@ window.__ModuleLoader__.load({
|
|
|
7809
10296
|
if (ui && ui.settings) {
|
|
7810
10297
|
delete ui.settings
|
|
7811
10298
|
}
|
|
10299
|
+
quickProviderApi = null
|
|
7812
10300
|
try {
|
|
7813
10301
|
if (fiber && typeof fiber.dispose === 'function') fiber.dispose()
|
|
7814
10302
|
} catch (error) {
|
|
@@ -7821,6 +10309,10 @@ window.__ModuleLoader__.load({
|
|
|
7821
10309
|
var body = document.body
|
|
7822
10310
|
body.setAttribute('data-dsh-claude-style', '')
|
|
7823
10311
|
setHostContext(ctx)
|
|
10312
|
+
// Bind the official settings form before anything reads a preference:
|
|
10313
|
+
// 0.1.7+ serves namespaces through `ctx.configForms`, and a host without
|
|
10314
|
+
// it keeps the plugin's own route. The choice is made once, here.
|
|
10315
|
+
adoptSettingsForm(ctx)
|
|
7824
10316
|
loadModelCopy()
|
|
7825
10317
|
loadUsername()
|
|
7826
10318
|
// Preferences are read asynchronously from the host settings namespace;
|
|
@@ -7844,9 +10336,12 @@ window.__ModuleLoader__.load({
|
|
|
7844
10336
|
teardowns.push(installCopy(ctx, ui))
|
|
7845
10337
|
teardowns.push(installPermissions(ctx, ui))
|
|
7846
10338
|
teardowns.push(installModelPicker(ctx, ui))
|
|
10339
|
+
teardowns.push(installHeroMenu(ctx, ui)) // hero 行的目录/预设弹层:打标记给样式表用
|
|
10340
|
+
teardowns.push(installQuickProviders(ctx, ui)) // 设置页的「快捷供应商」多选弹层
|
|
7847
10341
|
teardowns.push(installAccountFooter(ctx, ui))
|
|
7848
10342
|
teardowns.push(installBanScreen(ctx, ui)) // 账户横条的封号彩蛋(账户弹层把点击交给 ui.ban)
|
|
7849
10343
|
teardowns.push(installThemeFlip()) // 主题翻转瞬间抑制过渡,修掉「先色后样」
|
|
10344
|
+
teardowns.push(installWorkspaceView(ctx, ui)) // 侧栏工作区:进行中 / 已归档 分段 + 归档行删除
|
|
7850
10345
|
var stopSettings = installSettingsSection(ctx, ui)
|
|
7851
10346
|
teardowns.push(installScheduler(ctx, ui)) // 最后装,回调中惰性读 ui 句柄
|
|
7852
10347
|
|