dsh-style-tweaks 0.1.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/LICENSE +21 -0
- package/README.en.md +114 -0
- package/README.md +113 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +2379 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +90 -0
- package/lib/config.js.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/types/client/conversation-width.d.ts +81 -0
- package/lib/types/client/conversation-width.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +104 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/tweak-config.d.ts +55 -0
- package/lib/types/client/tweak-config.d.ts.map +1 -0
- package/lib/types/client/tweak-types.d.ts +32 -0
- package/lib/types/client/tweak-types.d.ts.map +1 -0
- package/lib/types/client/tweaks/code-block-flush-top.d.ts +91 -0
- package/lib/types/client/tweaks/code-block-flush-top.d.ts.map +1 -0
- package/lib/types/client/tweaks/locate-current-session.d.ts +62 -0
- package/lib/types/client/tweaks/locate-current-session.d.ts.map +1 -0
- package/lib/types/client/tweaks/project-running-indicator.d.ts +61 -0
- package/lib/types/client/tweaks/project-running-indicator.d.ts.map +1 -0
- package/lib/types/client/tweaks/registry.d.ts +22 -0
- package/lib/types/client/tweaks/registry.d.ts.map +1 -0
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts +168 -0
- package/lib/types/client/tweaks/settings-nav-scroll.d.ts.map +1 -0
- package/lib/types/client/tweaks/stable-table.d.ts +3 -0
- package/lib/types/client/tweaks/stable-table.d.ts.map +1 -0
- package/lib/types/client/tweaks/stable-turn-rail.d.ts +53 -0
- package/lib/types/client/tweaks/stable-turn-rail.d.ts.map +1 -0
- package/lib/types/config.d.ts +149 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/web.d.ts +55 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/web.js +169 -0
- package/lib/web.js.map +1 -0
- package/package.json +111 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2379 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-style-tweaks", factory: (require) => {
|
|
2
|
+
var __modules = Object.create(null); var __cache = Object.create(null);
|
|
3
|
+
__modules["./conversation-width.js"] = function(module, exports, require, __load_) {
|
|
4
|
+
"use strict";
|
|
5
|
+
/**
|
|
6
|
+
* dsh-style-tweaks — conversation-width override (browser half).
|
|
7
|
+
*
|
|
8
|
+
* When the user turns "plugin width control" on, this module:
|
|
9
|
+
*
|
|
10
|
+
* 1. Hides DSH's native 40 px hover drag handles. The handles live in a
|
|
11
|
+
* CSS-Modules file (`ConversationRoot.module.css`) whose class names are
|
|
12
|
+
* build-time-hashed, so a global stylesheet cannot target them by class —
|
|
13
|
+
* it MUST use the stable `data-width-handle` attribute selector.
|
|
14
|
+
*
|
|
15
|
+
* 2. Drives DSH's own shared width axis instead of forking it. The
|
|
16
|
+
* conversation root declares the single source of truth
|
|
17
|
+
* (`ConversationRoot.module.css`, `.root`):
|
|
18
|
+
*
|
|
19
|
+
* --dsh-chat-content-width: var(--dsh-chat-user-width, clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px));
|
|
20
|
+
* --dsh-composer-card-max-width: calc(var(--dsh-chat-content-width) + 32px);
|
|
21
|
+
*
|
|
22
|
+
* Every consumer — the transcript column (`data-chat-flow`), message
|
|
23
|
+
* bubbles, wide-table bleed, the back-to-bottom control, the composer
|
|
24
|
+
* card, and the dock/stats line — reads those two variables, so forcing
|
|
25
|
+
* `--dsh-chat-user-width` keeps the input card and the conversation area
|
|
26
|
+
* in lockstep by construction. The plugin never sets max-widths itself.
|
|
27
|
+
*
|
|
28
|
+
* 3. Reproduces DSH's narrow-viewport clamp in CSS. Natively
|
|
29
|
+
* (`ConversationRoot.tsx`, `resolveContentWidth`) a stored preference is
|
|
30
|
+
* re-clamped against the live column width — published as
|
|
31
|
+
* `--dsh-conversation-column-width` by a ResizeObserver — so opening the
|
|
32
|
+
* sidebar narrows the content instead of letting it overflow. This module
|
|
33
|
+
* declares the same clamp declaratively on the conversation root:
|
|
34
|
+
*
|
|
35
|
+
* --dsh-chat-user-width: min(<width>px, max(640px, calc(var(--dsh-conversation-column-width, 99999px) - 2 * <sideMargin>px)));
|
|
36
|
+
*
|
|
37
|
+
* i.e. the column never claims more than the dialog width, and never
|
|
38
|
+
* leaves less than `sideMargin` of whitespace on either side; 640px is
|
|
39
|
+
* DSH's own content floor (CONTENT_MIN). The var() substitution happens
|
|
40
|
+
* on the conversation root — the same element DSH writes the live column
|
|
41
|
+
* width on — so the clamp tracks sidebar toggles and window resizes with
|
|
42
|
+
* zero JS. The declaration is `!important` so it also beats DSH's inline
|
|
43
|
+
* re-publication of `--dsh-chat-user-width` on the same element.
|
|
44
|
+
*
|
|
45
|
+
* 4. Mirrors the chosen px into the shared localStorage slot
|
|
46
|
+
* (`dsh.conversation.contentWidth`) so toggling plugin-width OFF surfaces
|
|
47
|
+
* the user's last choice in the native drag handles — the switch is
|
|
48
|
+
* value-preserving in both directions.
|
|
49
|
+
*
|
|
50
|
+
* When the user turns the feature OFF, the installer disposes: the handle
|
|
51
|
+
* rule and the width-axis override are removed and the native handles take
|
|
52
|
+
* over from whatever px the user last picked.
|
|
53
|
+
*
|
|
54
|
+
* The conversation root cannot be targeted by a stable attribute of its own
|
|
55
|
+
* (it only carries `data-phase`), so the override anchors on the stable
|
|
56
|
+
* `data-conversation-scroll` descendant: `div:has([data-conversation-scroll])`
|
|
57
|
+
* matches exactly the ancestors of the scroll body, and the innermost
|
|
58
|
+
* declaration that matters — the conversation root — is where DSH publishes
|
|
59
|
+
* the live column width.
|
|
60
|
+
*
|
|
61
|
+
* ## Live-update contract
|
|
62
|
+
*
|
|
63
|
+
* `installConversationWidthStyles` returns a controller. `setWidth` mutates
|
|
64
|
+
* the same `<style>` element in place (no reinstall, no flicker) and is what
|
|
65
|
+
* the live settings subscription in `index.tsx` calls on every change. The
|
|
66
|
+
* `apply` effect MUST keep the same controller alive across settings
|
|
67
|
+
* changes — tearing it down and reinstalling on every tick would race with
|
|
68
|
+
* the user's viewport-driven sidebar drag and miss the very first frame.
|
|
69
|
+
* @module dsh-style-tweaks/client/conversation-width
|
|
70
|
+
*/
|
|
71
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72
|
+
exports.installConversationWidthStyles = installConversationWidthStyles;
|
|
73
|
+
const tweak_config_ts_1 = __load_("./tweak-config.js");
|
|
74
|
+
/** Stable attribute selector for the native WidthHandle strips (CSS-Modules-hashed class). */
|
|
75
|
+
const HANDLE_HIDE_RULE = '[data-width-handle]{display:none !important}';
|
|
76
|
+
/** DSH's own content floor (`ConversationRoot.tsx` CONTENT_MIN). */
|
|
77
|
+
const CONTENT_MIN_PX = 640;
|
|
78
|
+
/**
|
|
79
|
+
* CSS injected into <head> while plugin-width is on:
|
|
80
|
+
* - hide the native handles (hashed class → attribute selector)
|
|
81
|
+
* - clamp DSH's user-width preference to the plugin's dialog width, keeping
|
|
82
|
+
* at least `sideMargin` of whitespace per side as the live column narrows
|
|
83
|
+
* - mirror the raw px on :root (`--dsh-dialog-width-chat-width`) for any
|
|
84
|
+
* consumer that reads the legacy var from the document root.
|
|
85
|
+
*/
|
|
86
|
+
function buildWidthCss(widthPx, sideMargin) {
|
|
87
|
+
return `
|
|
88
|
+
${HANDLE_HIDE_RULE}
|
|
89
|
+
div:has([data-conversation-scroll]){--dsh-chat-user-width:min(${widthPx}px, max(${CONTENT_MIN_PX}px, calc(var(--dsh-conversation-column-width, 99999px) - ${sideMargin * 2}px))) !important}
|
|
90
|
+
:root{--dsh-dialog-width-chat-width:${widthPx}px}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Install the width-override stylesheet. Idempotent: a second call with the
|
|
95
|
+
* same id returns the existing controller.
|
|
96
|
+
* @param widthPx - the plugin's chosen column width in px.
|
|
97
|
+
* @param sideMargin - whitespace in px to keep on each side of the column.
|
|
98
|
+
* @returns a controller exposing `setWidth` + `dispose`.
|
|
99
|
+
*/
|
|
100
|
+
function installConversationWidthStyles(widthPx, sideMargin) {
|
|
101
|
+
const id = 'dsh-style-tweaks-conversation-width';
|
|
102
|
+
let style = document.querySelector(`style[data-plugin-css="${id}"]`);
|
|
103
|
+
if (style === null) {
|
|
104
|
+
style = document.createElement('style');
|
|
105
|
+
style.dataset.plugin = 'dsh-style-tweaks';
|
|
106
|
+
style.dataset.pluginCss = id;
|
|
107
|
+
document.head.appendChild(style);
|
|
108
|
+
}
|
|
109
|
+
// Persist to the shared storage slot so toggling plugin-width off surfaces
|
|
110
|
+
// the user's last choice in the native drag handles.
|
|
111
|
+
try {
|
|
112
|
+
localStorage.setItem(tweak_config_ts_1.CONVERSATION_WIDTH_STORAGE_KEY, `${widthPx}`);
|
|
113
|
+
}
|
|
114
|
+
catch { /* storage may be disabled; ignore */ }
|
|
115
|
+
// Track the latest applied width so dispose() can seed the conversation
|
|
116
|
+
// root with the value the user most recently picked, not the value at
|
|
117
|
+
// install time (setWidth mutates this; the constructor's widthPx would
|
|
118
|
+
// otherwise be stale across stepper clicks).
|
|
119
|
+
let currentWidth = widthPx;
|
|
120
|
+
const apply = (px, margin) => {
|
|
121
|
+
style.textContent = buildWidthCss(px, margin);
|
|
122
|
+
// Belt-and-suspenders: also set the var directly on :root in case any
|
|
123
|
+
// consumer reads the legacy var via JS rather than via CSS cascade.
|
|
124
|
+
document.documentElement.style.setProperty('--dsh-dialog-width-chat-width', `${px}px`);
|
|
125
|
+
};
|
|
126
|
+
apply(widthPx, sideMargin);
|
|
127
|
+
return {
|
|
128
|
+
setWidth: (px, margin) => {
|
|
129
|
+
currentWidth = px;
|
|
130
|
+
apply(px, margin);
|
|
131
|
+
try {
|
|
132
|
+
localStorage.setItem(tweak_config_ts_1.CONVERSATION_WIDTH_STORAGE_KEY, `${px}`);
|
|
133
|
+
}
|
|
134
|
+
catch { /* ignore */ }
|
|
135
|
+
},
|
|
136
|
+
dispose: () => {
|
|
137
|
+
// Before removing the !important clamp rule, seed --dsh-chat-user-width
|
|
138
|
+
// directly on the conversation root so the column keeps the user's
|
|
139
|
+
// chosen width instead of falling back to DSH's adaptive clamp
|
|
140
|
+
// (resolveContentWidth would otherwise shrink it to fit the live
|
|
141
|
+
// column, which is wrong when the user explicitly chose a wider value
|
|
142
|
+
// and just wants to hand control back to the native drag handles).
|
|
143
|
+
// The root is identified by [data-phase] (ConversationRoot.tsx) and is
|
|
144
|
+
// the same element DSH writes the var on in publishWidths().
|
|
145
|
+
const root = document.querySelector('[data-conversation-scroll]')?.closest('[data-phase]') ?? null;
|
|
146
|
+
if (root !== null) {
|
|
147
|
+
root.style.setProperty('--dsh-chat-user-width', `${currentWidth}px`);
|
|
148
|
+
}
|
|
149
|
+
style?.remove();
|
|
150
|
+
document.documentElement.style.removeProperty('--dsh-dialog-width-chat-width');
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
__modules["./index.js"] = function(module, exports, require, __load_) {
|
|
156
|
+
"use strict";
|
|
157
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
158
|
+
exports.inject = exports.SettingsClient = void 0;
|
|
159
|
+
exports.apply = apply;
|
|
160
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
161
|
+
/**
|
|
162
|
+
* dsh-style-tweaks — browser half.
|
|
163
|
+
*
|
|
164
|
+
* Owns the runtime CSS that drives two feature areas under one Settings
|
|
165
|
+
* panel:
|
|
166
|
+
* 1. Column-width control (ported from dsh-dialog-width): px stepper,
|
|
167
|
+
* presets, plugin-vs-native toggle, side margin.
|
|
168
|
+
* 2. Opt-in CSS tweaks: stable-table layout on hover, with more added
|
|
169
|
+
* over time. Each tweak is a boolean field.
|
|
170
|
+
*
|
|
171
|
+
* Reads and writes the `style-tweaks` settings namespace via
|
|
172
|
+
* the same-origin route served by the server half.
|
|
173
|
+
*/
|
|
174
|
+
const react_1 = require("react");
|
|
175
|
+
const react_dom_1 = require("react-dom");
|
|
176
|
+
const conversation_width_ts_1 = __load_("./conversation-width.js");
|
|
177
|
+
const tweak_config_ts_1 = __load_("./tweak-config.js");
|
|
178
|
+
const registry_ts_1 = __load_("./tweaks/registry.js");
|
|
179
|
+
const stable_table_ts_1 = __load_("./tweaks/stable-table.js");
|
|
180
|
+
const stable_turn_rail_ts_1 = __load_("./tweaks/stable-turn-rail.js");
|
|
181
|
+
const code_block_flush_top_ts_1 = __load_("./tweaks/code-block-flush-top.js");
|
|
182
|
+
const project_running_indicator_ts_1 = __load_("./tweaks/project-running-indicator.js");
|
|
183
|
+
const locate_current_session_ts_1 = __load_("./tweaks/locate-current-session.js");
|
|
184
|
+
const settings_nav_scroll_ts_1 = __load_("./tweaks/settings-nav-scroll.js");
|
|
185
|
+
const NS = 'style-tweaks';
|
|
186
|
+
const SETTINGS_ROUTE = '/_dsh/style-tweaks/settings';
|
|
187
|
+
/**
|
|
188
|
+
* Maps a tweak id to its mount function. Add new tweaks here. Pure-CSS
|
|
189
|
+
* injectors ignore the context; JS-level tweaks (DOM patching driven by app
|
|
190
|
+
* stores) receive it to read services like `sessions` / `workspaces`.
|
|
191
|
+
*/
|
|
192
|
+
const TWEAK_INJECTORS = {
|
|
193
|
+
'stable-table': () => (0, stable_table_ts_1.injectStableTableStyles)(),
|
|
194
|
+
'stable-turn-rail': () => (0, stable_turn_rail_ts_1.injectStableTurnRailStyles)(),
|
|
195
|
+
'code-block-flush-top': () => (0, code_block_flush_top_ts_1.injectCodeBlockFlushTopStyles)(),
|
|
196
|
+
'project-running-indicator': project_running_indicator_ts_1.setupProjectRunningIndicator,
|
|
197
|
+
'locate-current-session': locate_current_session_ts_1.setupLocateCurrentSession,
|
|
198
|
+
'settings-nav-scroll': settings_nav_scroll_ts_1.setupSettingsNavScroll,
|
|
199
|
+
};
|
|
200
|
+
const en = {
|
|
201
|
+
nav: 'Style tweaks',
|
|
202
|
+
settingsTitle: 'Style tweaks',
|
|
203
|
+
settingsIntro: 'Opt-in style tweaks for DSH: precise conversation column-width control (with presets and side margin) plus a set of small fixes for the sidebar and the settings panel. Each toggle applies immediately and persists to your settings document.',
|
|
204
|
+
sectionLayout: 'Layout',
|
|
205
|
+
sectionTweaks: 'Tweaks',
|
|
206
|
+
dialogWidth: 'Dialog width',
|
|
207
|
+
dialogWidthHint: 'Number between 600 and 1600 px; 748 is DSH\'s default column width, larger values widen it.',
|
|
208
|
+
presetDefault: 'Default',
|
|
209
|
+
presetWide: 'Wide',
|
|
210
|
+
presetWideXl: 'Extra wide',
|
|
211
|
+
usePluginWidth: 'Plugin width control',
|
|
212
|
+
usePluginWidthHint: 'When ON, the width input / presets above drive the column and DSH\'s native drag handles are hidden. When OFF, DSH\'s native handles own the column; the width input mirrors their value.',
|
|
213
|
+
usePluginWidthOn: 'On',
|
|
214
|
+
usePluginWidthOff: 'Off',
|
|
215
|
+
sideMargin: 'Side margin',
|
|
216
|
+
sideMarginHint: 'Whitespace in px kept on each side of the conversation area. The column is clamped to the dialog width and narrows when the sidebar opens or the window shrinks, never hugging the edges. Minimum 32 px.',
|
|
217
|
+
defaultAction: 'Default',
|
|
218
|
+
applied: 'Applied',
|
|
219
|
+
unavailable: 'Settings unavailable.',
|
|
220
|
+
loading: 'Loading…',
|
|
221
|
+
readOnly: 'The active Settings provider is read-only.',
|
|
222
|
+
tweakOn: 'On',
|
|
223
|
+
tweakOff: 'Off',
|
|
224
|
+
'tweak.stableTable.title': 'Stable table layout',
|
|
225
|
+
'tweak.stableTable.description': 'Lock table layout on hover so surrounding content does not reflow ("text jumps when I hover a table").',
|
|
226
|
+
'tweak.stableTurnRail.title': 'Stable turn-navigation rail',
|
|
227
|
+
'tweak.stableTurnRail.description': 'Keep the turn-navigation rail at a stable position when scrolling up past the first message into the system prompt ("the rail jumps down by ~16 px when I scroll up after clicking the first turn").',
|
|
228
|
+
'tweak.codeBlockFlushTop.title': 'Flush code-block top',
|
|
229
|
+
'tweak.codeBlockFlushTop.description': 'Remove the 16 px gap above highlighted code blocks so the code sits flush with the preceding paragraph, list item, or heading.',
|
|
230
|
+
'tweak.projectRunningIndicator.title': 'Project running indicator',
|
|
231
|
+
'tweak.projectRunningIndicator.description': 'Show the conversation title\'s animated running dot on the right side of each project directory in the sidebar, so a running conversation stays visible even when the directory is collapsed.',
|
|
232
|
+
'tweak.locateCurrentSession.title': 'Locate current session',
|
|
233
|
+
'tweak.locateCurrentSession.description': 'Add a "locate" button next to the sidebar search box. Click it to expand the current session\'s workspace and scroll the session into view.',
|
|
234
|
+
'tweak.settingsNavScroll.title': 'Scrollable settings nav',
|
|
235
|
+
'tweak.settingsNavScroll.description': 'Let the settings dialog\'s left menu scroll when its entries outgrow the panel, instead of silently clipping the ones at the bottom.',
|
|
236
|
+
};
|
|
237
|
+
const zh = {
|
|
238
|
+
nav: '样式调整',
|
|
239
|
+
settingsTitle: '样式调整',
|
|
240
|
+
settingsIntro: 'DSH 界面的可选样式调整:对话列宽精确控制(含预设与两侧边距),以及侧边栏与设置面板的一组小幅修复。每个开关立即生效并持久化到设置文档。',
|
|
241
|
+
sectionLayout: '布局',
|
|
242
|
+
sectionTweaks: '调整项',
|
|
243
|
+
dialogWidth: '对话框宽度',
|
|
244
|
+
dialogWidthHint: '取值 600–1600 px;748 为 DSH 默认列宽,数字越大越宽。',
|
|
245
|
+
presetDefault: '默认',
|
|
246
|
+
presetWide: '稍宽',
|
|
247
|
+
presetWideXl: '更宽',
|
|
248
|
+
usePluginWidth: '插件宽度控制',
|
|
249
|
+
usePluginWidthHint: '开启时,上方宽度输入 / 预设驱动列宽,并隐藏 DSH 原生的拖拽手柄;关闭时,DSH 原生手柄接管列宽,宽度输入同步显示当前值。',
|
|
250
|
+
usePluginWidthOn: '开启',
|
|
251
|
+
usePluginWidthOff: '关闭',
|
|
252
|
+
sideMargin: '两侧边距',
|
|
253
|
+
sideMarginHint: '对话区域两侧保留的空白(px)。列宽被钳制为对话框宽度,侧边栏打开或窗口缩小时内容会收窄,不会贴住边缘。最低 32 px。',
|
|
254
|
+
defaultAction: '默认',
|
|
255
|
+
applied: '已应用',
|
|
256
|
+
unavailable: '设置暂不可用。',
|
|
257
|
+
loading: '加载中…',
|
|
258
|
+
readOnly: '当前设置提供方为只读。',
|
|
259
|
+
tweakOn: '开启',
|
|
260
|
+
tweakOff: '关闭',
|
|
261
|
+
'tweak.stableTable.title': '表格布局稳定',
|
|
262
|
+
'tweak.stableTable.description': '锁住表格 hover 时的布局,避免周围内容发生回流("鼠标移到表格上时下方文本会跳动")。',
|
|
263
|
+
'tweak.stableTurnRail.title': '轮次导航栏稳定',
|
|
264
|
+
'tweak.stableTurnRail.description': '向上滚动到第一条消息上方的系统提示词区域时,让右侧轮次导航栏保持在原位(不再下移约 16 像素)。',
|
|
265
|
+
'tweak.codeBlockFlushTop.title': '代码块顶部贴齐',
|
|
266
|
+
'tweak.codeBlockFlushTop.description': '去掉高亮代码块上方的 16 px 空白,让代码块紧贴在前面的段落、列表项或标题下方。',
|
|
267
|
+
'tweak.projectRunningIndicator.title': '项目目录运行指示',
|
|
268
|
+
'tweak.projectRunningIndicator.description': '在侧边栏项目目录右侧显示与对话标题一致的运行动画圆点,目录收起时也能一眼看出里面有对话正在进行。',
|
|
269
|
+
'tweak.locateCurrentSession.title': '定位当前会话',
|
|
270
|
+
'tweak.locateCurrentSession.description': '在侧边栏搜索框旁边添加一个"定位"按钮。点击后展开当前会话所属的工作区目录,并将该会话滚动到侧边栏视口内。',
|
|
271
|
+
'tweak.settingsNavScroll.title': '设置菜单可滚动',
|
|
272
|
+
'tweak.settingsNavScroll.description': '设置项较多时,让设置面板左侧菜单可以上下滚动,而不是把放不下的项直接裁掉。',
|
|
273
|
+
};
|
|
274
|
+
function resolveValue(value) {
|
|
275
|
+
return {
|
|
276
|
+
dialogWidth: (0, tweak_config_ts_1.resolveDialogWidth)(value?.dialogWidth),
|
|
277
|
+
usePluginWidth: value?.usePluginWidth ?? true,
|
|
278
|
+
sideMargin: (0, tweak_config_ts_1.resolveSideMargin)(value?.sideMargin),
|
|
279
|
+
stableTable: value?.stableTable ?? true,
|
|
280
|
+
stableTurnRail: value?.stableTurnRail ?? true,
|
|
281
|
+
codeBlockFlushTop: value?.codeBlockFlushTop ?? true,
|
|
282
|
+
projectRunningIndicator: value?.projectRunningIndicator ?? true,
|
|
283
|
+
locateCurrentSession: value?.locateCurrentSession ?? true,
|
|
284
|
+
settingsNavScroll: value?.settingsNavScroll ?? true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const BASE_CSS = `
|
|
288
|
+
.cst-settings{display:grid;gap:8px;max-width:680px;padding:4px 2px 24px;color:var(--dsw-alias-label-primary)}
|
|
289
|
+
.cst-settings-header{display:flex;align-items:flex-start;gap:10px;padding:2px 2px 0}
|
|
290
|
+
.cst-logo{flex:none;display:grid;place-items:center;width:30px;height:30px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1);background:linear-gradient(135deg,color-mix(in srgb,var(--dsw-alias-state-business-primary) 16%,transparent),transparent);font-size:15px;line-height:1}
|
|
291
|
+
.cst-settings-header h2{font-size:16px;letter-spacing:-.01em;margin:0 0 2px}
|
|
292
|
+
.cst-settings-header p{max-width:600px;margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45}
|
|
293
|
+
.cst-panel{display:grid;gap:0;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1);overflow:hidden}
|
|
294
|
+
.cst-panel+.cst-panel{margin-top:10px}
|
|
295
|
+
.cst-section-label{font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);padding:9px 16px 4px}
|
|
296
|
+
.cst-field{display:grid;gap:6px;padding:7px 16px 10px}
|
|
297
|
+
.cst-field+.cst-field{border-top:1px solid var(--dsw-alias-border-l1)}
|
|
298
|
+
.cst-field-top{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
|
|
299
|
+
.cst-field-top>span{font-size:13.5px;font-weight:600}
|
|
300
|
+
.cst-label{display:inline-flex;align-items:center;gap:6px}
|
|
301
|
+
.cst-hint{flex:none;display:inline-grid;place-items:center;width:15px;height:15px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:9.5px;font-weight:600;font-style:normal;line-height:1;cursor:help;user-select:none;transition:color .15s ease,border-color .15s ease}
|
|
302
|
+
.cst-hint:hover,.cst-hint:focus-visible{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
|
|
303
|
+
.cst-hint-pop{position:fixed;z-index:9999;width:max-content;max-width:300px;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:11.5px;line-height:1.5;box-shadow:0 4px 16px rgba(0,0,0,.14);pointer-events:none}
|
|
304
|
+
.cst-controls{display:flex;align-items:center;gap:8px}
|
|
305
|
+
.cst-stepper{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
306
|
+
.cst-stepper button{width:28px;height:28px;border:none;background:transparent;color:inherit;font-size:15px;font-weight:500;line-height:1;cursor:pointer;display:grid;place-items:center;transition:background .15s ease}
|
|
307
|
+
.cst-stepper button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
308
|
+
.cst-stepper button:disabled{opacity:.35;cursor:default}
|
|
309
|
+
.cst-stepper input{box-sizing:border-box;width:60px;height:28px;border:none;border-left:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);background:transparent;color:inherit;font:inherit;font-size:13px;text-align:center;-moz-appearance:textfield}
|
|
310
|
+
.cst-stepper input::-webkit-outer-spin-button,.cst-stepper input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
311
|
+
.cst-stepper input:focus{outline:none}
|
|
312
|
+
.cst-seg{display:inline-flex;padding:3px;gap:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
|
|
313
|
+
.cst-seg button{border:none;border-radius:7px;padding:5px 12px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer;transition:background .15s ease,color .15s ease}
|
|
314
|
+
.cst-seg button:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
315
|
+
.cst-seg button.cst-seg-active{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600;box-shadow:none}
|
|
316
|
+
.cst-seg button.cst-seg-active:hover:not(:disabled){color:var(--dsw-alias-state-business-primary)}
|
|
317
|
+
.cst-seg button:disabled{opacity:.45;cursor:default}
|
|
318
|
+
.cst-presets{display:inline-flex;flex-wrap:wrap;margin-top:2px}
|
|
319
|
+
.cst-toast-wrap{display:flex;justify-content:flex-end;margin-top:8px;margin-right:18px}
|
|
320
|
+
.cst-toast{display:inline-flex;align-items:center;gap:7px;font-size:11.5px;font-weight:500;line-height:1;padding:6px 11px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary);animation:cst-toast-in .22s cubic-bezier(.2,.7,.3,1)}
|
|
321
|
+
.cst-toast.error{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}
|
|
322
|
+
.cst-toast-dot{flex:none;width:5px;height:5px;border-radius:50%;background:currentColor}
|
|
323
|
+
@keyframes cst-toast-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
324
|
+
.cst-loading{padding:16px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
325
|
+
.cst-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5}
|
|
326
|
+
.cst-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}
|
|
327
|
+
.cst-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
|
|
328
|
+
`;
|
|
329
|
+
function installBaseStyles() {
|
|
330
|
+
const id = 'dsh-style-tweaks-base';
|
|
331
|
+
let style = document.querySelector(`style[data-plugin-css="${id}"]`);
|
|
332
|
+
if (style === null) {
|
|
333
|
+
style = document.createElement('style');
|
|
334
|
+
style.dataset.plugin = 'dsh-style-tweaks';
|
|
335
|
+
style.dataset.pluginCss = id;
|
|
336
|
+
style.textContent = BASE_CSS;
|
|
337
|
+
document.head.appendChild(style);
|
|
338
|
+
}
|
|
339
|
+
return () => { style?.remove(); };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Settings requests retry briefly on 502/503: writing the profile patch
|
|
343
|
+
* hot-reloads the `web` node and restarts this plugin for a moment (it
|
|
344
|
+
* injects `web`), so a toggle click can land inside that window. The retry
|
|
345
|
+
* rides it out instead of surfacing "settings unavailable".
|
|
346
|
+
*/
|
|
347
|
+
async function apiRequest(init) {
|
|
348
|
+
let lastError;
|
|
349
|
+
for (let attempt = 1; attempt <= 6; attempt++) {
|
|
350
|
+
try {
|
|
351
|
+
const response = await fetch(SETTINGS_ROUTE, { credentials: 'same-origin', ...init });
|
|
352
|
+
const body = await response.json();
|
|
353
|
+
if (response.ok && body.ok)
|
|
354
|
+
return body.value;
|
|
355
|
+
const failure = body;
|
|
356
|
+
const retryable = response.status === 502 || response.status === 503;
|
|
357
|
+
lastError = new Error(failure.error?.message ?? `Style tweaks request failed with HTTP ${response.status}`);
|
|
358
|
+
if (!retryable)
|
|
359
|
+
throw lastError;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
lastError = error;
|
|
363
|
+
}
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 250));
|
|
365
|
+
}
|
|
366
|
+
throw lastError ?? new Error('Style tweaks request failed');
|
|
367
|
+
}
|
|
368
|
+
/** Small external store shared by the Settings route and the CSS engine. */
|
|
369
|
+
class SettingsClient {
|
|
370
|
+
state = { status: 'loading', writable: false, value: undefined, revision: undefined };
|
|
371
|
+
listeners = new Set();
|
|
372
|
+
generation = 0;
|
|
373
|
+
subscribe = (listener) => {
|
|
374
|
+
this.listeners.add(listener);
|
|
375
|
+
return () => { this.listeners.delete(listener); };
|
|
376
|
+
};
|
|
377
|
+
getSnapshot = () => this.state;
|
|
378
|
+
publish(next) {
|
|
379
|
+
this.state = next;
|
|
380
|
+
for (const listener of this.listeners)
|
|
381
|
+
listener();
|
|
382
|
+
}
|
|
383
|
+
async load() {
|
|
384
|
+
const generation = ++this.generation;
|
|
385
|
+
if (this.state.status === 'loading')
|
|
386
|
+
this.publish({ ...this.state, status: 'loading' });
|
|
387
|
+
try {
|
|
388
|
+
const snapshot = await apiRequest();
|
|
389
|
+
if (generation !== this.generation)
|
|
390
|
+
return;
|
|
391
|
+
this.publish({
|
|
392
|
+
status: 'ready',
|
|
393
|
+
writable: snapshot.writable,
|
|
394
|
+
value: snapshot.value,
|
|
395
|
+
revision: snapshot.revision,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (generation !== this.generation)
|
|
400
|
+
return;
|
|
401
|
+
this.publish({ ...this.state, status: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async post(payload) {
|
|
405
|
+
const generation = ++this.generation;
|
|
406
|
+
const snapshot = await apiRequest({
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
body: JSON.stringify(payload),
|
|
410
|
+
});
|
|
411
|
+
if (generation !== this.generation)
|
|
412
|
+
return;
|
|
413
|
+
this.publish({
|
|
414
|
+
status: 'ready',
|
|
415
|
+
writable: snapshot.writable,
|
|
416
|
+
value: snapshot.value,
|
|
417
|
+
revision: snapshot.revision,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async set(field, value) {
|
|
421
|
+
await this.post({ action: 'set', field, value, expectedRevision: this.state.revision ?? 0 });
|
|
422
|
+
}
|
|
423
|
+
async unset(field) {
|
|
424
|
+
await this.post({ action: 'unset', field, expectedRevision: this.state.revision ?? 0 });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
exports.SettingsClient = SettingsClient;
|
|
428
|
+
/** Required client services: slots (settings.section), locale, and the app stores the JS-level tweaks read. */
|
|
429
|
+
exports.inject = ['slots', 'locale', 'sessions', 'workspaces'];
|
|
430
|
+
/**
|
|
431
|
+
* Hover/focus hint: a small ⓘ next to the field label; the hint text renders
|
|
432
|
+
* in a fixed-position bubble portaled to <body> (so panel `overflow:hidden`
|
|
433
|
+
* can never clip it), measured in a layout effect to prefer the space above
|
|
434
|
+
* the anchor and flip below near the viewport top. No layout shift: hints
|
|
435
|
+
* never occupy flow height.
|
|
436
|
+
*/
|
|
437
|
+
function Hint({ text }) {
|
|
438
|
+
const anchorRef = (0, react_1.useRef)(null);
|
|
439
|
+
const popRef = (0, react_1.useRef)(null);
|
|
440
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
441
|
+
const [pos, setPos] = (0, react_1.useState)({ top: -9999, left: -9999 });
|
|
442
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
443
|
+
if (!open)
|
|
444
|
+
return;
|
|
445
|
+
const anchor = anchorRef.current?.getBoundingClientRect();
|
|
446
|
+
const pop = popRef.current;
|
|
447
|
+
if (anchor === undefined || pop === null)
|
|
448
|
+
return;
|
|
449
|
+
let left = Math.min(Math.max(8, anchor.left), window.innerWidth - pop.offsetWidth - 8);
|
|
450
|
+
let top = anchor.top - pop.offsetHeight - 8;
|
|
451
|
+
if (top < 8)
|
|
452
|
+
top = anchor.bottom + 8;
|
|
453
|
+
setPos({ top, left });
|
|
454
|
+
}, [open]);
|
|
455
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: anchorRef, className: "cst-hint", role: "note", "aria-label": text, tabIndex: 0, onMouseEnter: () => { setOpen(true); }, onMouseLeave: () => { setOpen(false); }, onFocus: () => { setOpen(true); }, onBlur: () => { setOpen(false); }, children: "i" }), open && (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { ref: popRef, className: "cst-hint-pop", style: { top: pos.top, left: pos.left }, children: text }), document.body)] }));
|
|
456
|
+
}
|
|
457
|
+
function SettingsSection({ controller, t }) {
|
|
458
|
+
const state = (0, react_1.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
459
|
+
const resolved = resolveValue(state.value);
|
|
460
|
+
const writable = state.writable;
|
|
461
|
+
const [status, setStatus] = (0, react_1.useState)(undefined);
|
|
462
|
+
(0, react_1.useEffect)(() => { if (state.status === 'loading' && state.value === undefined)
|
|
463
|
+
void controller.load(); }, [controller, state.status, state.value]);
|
|
464
|
+
(0, react_1.useEffect)(() => {
|
|
465
|
+
if (status === undefined)
|
|
466
|
+
return;
|
|
467
|
+
const timer = setTimeout(() => { setStatus(undefined); }, 1800);
|
|
468
|
+
return () => { clearTimeout(timer); };
|
|
469
|
+
}, [status]);
|
|
470
|
+
const [widthDraft, setWidthDraft] = (0, react_1.useState)(String(resolved.dialogWidth));
|
|
471
|
+
const [marginDraft, setMarginDraft] = (0, react_1.useState)(String(resolved.sideMargin));
|
|
472
|
+
(0, react_1.useEffect)(() => { setWidthDraft(String(resolved.dialogWidth)); }, [resolved.dialogWidth]);
|
|
473
|
+
(0, react_1.useEffect)(() => { setMarginDraft(String(resolved.sideMargin)); }, [resolved.sideMargin]);
|
|
474
|
+
const commitDialogWidth = (raw) => {
|
|
475
|
+
setWidthDraft(raw);
|
|
476
|
+
const parsed = Number(raw);
|
|
477
|
+
if (!Number.isFinite(parsed))
|
|
478
|
+
return;
|
|
479
|
+
const clamped = Math.min(tweak_config_ts_1.MAX_DIALOG_WIDTH, Math.max(tweak_config_ts_1.MIN_DIALOG_WIDTH, Math.round(parsed)));
|
|
480
|
+
setWidthDraft(String(clamped));
|
|
481
|
+
void controller.set('dialogWidth', clamped).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
482
|
+
};
|
|
483
|
+
const stepDialogWidth = (delta) => {
|
|
484
|
+
const next = Math.min(tweak_config_ts_1.MAX_DIALOG_WIDTH, Math.max(tweak_config_ts_1.MIN_DIALOG_WIDTH, resolved.dialogWidth + delta));
|
|
485
|
+
setWidthDraft(String(next));
|
|
486
|
+
void controller.set('dialogWidth', next).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
487
|
+
};
|
|
488
|
+
const applyWidthPreset = (width) => {
|
|
489
|
+
setWidthDraft(String(width));
|
|
490
|
+
void controller.set('dialogWidth', width).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
491
|
+
};
|
|
492
|
+
const setUsePluginWidth = (value) => {
|
|
493
|
+
void controller.set('usePluginWidth', value).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
494
|
+
};
|
|
495
|
+
const commitSideMargin = (raw) => {
|
|
496
|
+
setMarginDraft(raw);
|
|
497
|
+
const parsed = Number(raw);
|
|
498
|
+
if (!Number.isFinite(parsed))
|
|
499
|
+
return;
|
|
500
|
+
const clamped = Math.max(tweak_config_ts_1.MIN_SIDE_MARGIN, Math.round(parsed));
|
|
501
|
+
setMarginDraft(String(clamped));
|
|
502
|
+
void controller.set('sideMargin', clamped).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
503
|
+
};
|
|
504
|
+
const stepSideMargin = (delta) => {
|
|
505
|
+
const next = Math.max(tweak_config_ts_1.MIN_SIDE_MARGIN, resolved.sideMargin + delta);
|
|
506
|
+
setMarginDraft(String(next));
|
|
507
|
+
void controller.set('sideMargin', next).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
508
|
+
};
|
|
509
|
+
const setTweak = (tweak, value) => {
|
|
510
|
+
void controller.set(tweak.settingKey, value).then(() => { setStatus('applied'); }).catch(() => { setStatus('unavailable'); });
|
|
511
|
+
};
|
|
512
|
+
if (state.status === 'loading' && state.value === undefined) {
|
|
513
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "cst-settings", children: (0, jsx_runtime_1.jsx)("div", { className: "cst-loading", children: t('loading') }) });
|
|
514
|
+
}
|
|
515
|
+
if (state.status === 'error') {
|
|
516
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "cst-settings", children: (0, jsx_runtime_1.jsx)("div", { className: "cst-alert error", children: t('unavailable') }) });
|
|
517
|
+
}
|
|
518
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "cst-settings", children: [(0, jsx_runtime_1.jsxs)("header", { className: "cst-settings-header", children: [(0, jsx_runtime_1.jsx)("div", { className: "cst-logo", children: "\uD83C\uDFA8" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { children: t('settingsTitle') }), (0, jsx_runtime_1.jsx)("p", { children: t('settingsIntro') })] })] }), !writable ? (0, jsx_runtime_1.jsx)("div", { className: "cst-alert warning", children: t('readOnly') }) : null, (0, jsx_runtime_1.jsxs)("section", { className: "cst-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "cst-section-label", children: t('sectionLayout') }), (0, jsx_runtime_1.jsx)("div", { className: "cst-field", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-field-top", children: [(0, jsx_runtime_1.jsxs)("span", { className: "cst-label", children: [t('usePluginWidth'), (0, jsx_runtime_1.jsx)(Hint, { text: t('usePluginWidthHint') })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: resolved.usePluginWidth ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { setUsePluginWidth(true); }, children: t('usePluginWidthOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !resolved.usePluginWidth ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { setUsePluginWidth(false); }, children: t('usePluginWidthOff') })] }) })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "cst-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "cst-field-top", children: [(0, jsx_runtime_1.jsxs)("span", { className: "cst-label", children: [t('dialogWidth'), (0, jsx_runtime_1.jsx)(Hint, { text: t('dialogWidthHint') })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-stepper", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "\u2212", disabled: !writable || resolved.dialogWidth <= tweak_config_ts_1.MIN_DIALOG_WIDTH, onClick: () => { stepDialogWidth(-20); }, children: "\u2212" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: tweak_config_ts_1.MIN_DIALOG_WIDTH, max: tweak_config_ts_1.MAX_DIALOG_WIDTH, step: 20, value: widthDraft, disabled: !writable, onChange: (event) => { setWidthDraft(event.target.value); }, onBlur: (event) => { commitDialogWidth(event.target.value); }, onKeyDown: (event) => { if (event.key === 'Enter')
|
|
519
|
+
commitDialogWidth(event.target.value); } }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "+", disabled: !writable || resolved.dialogWidth >= tweak_config_ts_1.MAX_DIALOG_WIDTH, onClick: () => { stepDialogWidth(20); }, children: "+" })] }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-presets", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-seg", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 880 ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(880); }, children: [t('presetWide'), " \u00B7 880"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 1024 ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(1024); }, children: [t('presetWideXl'), " \u00B7 1024"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: resolved.dialogWidth === 748 ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { applyWidthPreset(748); }, children: [t('presetDefault'), " \u00B7 748"] })] }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-field", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-field-top", children: [(0, jsx_runtime_1.jsxs)("span", { className: "cst-label", children: [t('sideMargin'), (0, jsx_runtime_1.jsx)(Hint, { text: t('sideMarginHint') })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-stepper", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "\u2212", disabled: !writable || resolved.sideMargin <= tweak_config_ts_1.MIN_SIDE_MARGIN, onClick: () => { stepSideMargin(-4); }, children: "\u2212" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: tweak_config_ts_1.MIN_SIDE_MARGIN, step: 4, value: marginDraft, disabled: !writable, onChange: (event) => { setMarginDraft(event.target.value); }, onBlur: (event) => { commitSideMargin(event.target.value); }, onKeyDown: (event) => { if (event.key === 'Enter')
|
|
520
|
+
commitSideMargin(event.target.value); } }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": "+", disabled: !writable, onClick: () => { stepSideMargin(4); }, children: "+" })] }) })] }) })] }), (0, jsx_runtime_1.jsxs)("section", { className: "cst-panel", children: [(0, jsx_runtime_1.jsx)("div", { className: "cst-section-label", children: t('sectionTweaks') }), registry_ts_1.TWEAKS.map(tweak => {
|
|
521
|
+
const enabled = resolved[tweak.settingKey] ?? tweak.defaultEnabled;
|
|
522
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "cst-field", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-field-top", children: [(0, jsx_runtime_1.jsxs)("span", { className: "cst-label", children: [t(tweak.titleKey), (0, jsx_runtime_1.jsx)(Hint, { text: t(tweak.descriptionKey) })] }), (0, jsx_runtime_1.jsx)("div", { className: "cst-controls", children: (0, jsx_runtime_1.jsxs)("div", { className: "cst-seg", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: enabled ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { setTweak(tweak, true); }, children: t('tweakOn') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: !enabled ? 'cst-seg-active' : '', disabled: !writable, onClick: () => { setTweak(tweak, false); }, children: t('tweakOff') })] }) })] }) }, tweak.id));
|
|
523
|
+
})] }), status === undefined ? null : ((0, jsx_runtime_1.jsx)("div", { className: "cst-toast-wrap", children: (0, jsx_runtime_1.jsxs)("div", { className: 'cst-toast' + (status === 'unavailable' ? ' error' : ''), role: "status", "aria-live": "polite", children: [(0, jsx_runtime_1.jsx)("span", { className: "cst-toast-dot", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: t(status) })] }) }))] }));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Sanity-check that every tweak in the registry has matching `titleKey` /
|
|
527
|
+
* `descriptionKey` strings in both `en` and `zh`. Catches "added a tweak
|
|
528
|
+
* but forgot the i18n strings" at apply time instead of as an untranslated
|
|
529
|
+
* label visible to users.
|
|
530
|
+
*/
|
|
531
|
+
function assertTweakI18nComplete() {
|
|
532
|
+
const missing = (locale, table) => {
|
|
533
|
+
const out = [];
|
|
534
|
+
for (const tweak of registry_ts_1.TWEAKS) {
|
|
535
|
+
if (table[tweak.titleKey] === undefined)
|
|
536
|
+
out.push(`${locale}:${tweak.titleKey}`);
|
|
537
|
+
if (table[tweak.descriptionKey] === undefined)
|
|
538
|
+
out.push(`${locale}:${tweak.descriptionKey}`);
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
};
|
|
542
|
+
const problems = [...missing('en', en), ...missing('zh', zh)];
|
|
543
|
+
if (problems.length > 0) {
|
|
544
|
+
console.error('[dsh-style-tweaks] missing i18n keys:', problems);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function apply(ctx) {
|
|
548
|
+
assertTweakI18nComplete();
|
|
549
|
+
ctx.effect(installBaseStyles, 'dsh-style-tweaks: base styles');
|
|
550
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-style-tweaks: locale');
|
|
551
|
+
const t = ctx.locale.bind(NS);
|
|
552
|
+
const controller = new SettingsClient();
|
|
553
|
+
// Width-axis override: when plugin-width is on, install the handle-hiding
|
|
554
|
+
// + user-width-clamp CSS once and keep the same controller alive; on
|
|
555
|
+
// every settings change, mutate it in place via `setWidth` (no reinstall,
|
|
556
|
+
// no flicker — DSH's own `publishWidths` races the install on every
|
|
557
|
+
// ResizeObserver tick and would otherwise drop the first frame). When the
|
|
558
|
+
// user toggles plugin-width off, dispose the controller so DSH's native
|
|
559
|
+
// handles take back over.
|
|
560
|
+
ctx.effect(() => {
|
|
561
|
+
let widthController;
|
|
562
|
+
const sync = () => {
|
|
563
|
+
const value = controller.getSnapshot().value;
|
|
564
|
+
const usePlugin = value?.usePluginWidth ?? true;
|
|
565
|
+
const width = (0, tweak_config_ts_1.resolveDialogWidth)(value?.dialogWidth);
|
|
566
|
+
const sideMargin = (0, tweak_config_ts_1.resolveSideMargin)(value?.sideMargin);
|
|
567
|
+
if (usePlugin) {
|
|
568
|
+
if (widthController === undefined) {
|
|
569
|
+
widthController = (0, conversation_width_ts_1.installConversationWidthStyles)(width, sideMargin);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
widthController.setWidth(width, sideMargin);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else if (widthController !== undefined) {
|
|
576
|
+
widthController.dispose();
|
|
577
|
+
widthController = undefined;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
sync();
|
|
581
|
+
void controller.load();
|
|
582
|
+
return controller.subscribe(sync);
|
|
583
|
+
}, 'dsh-style-tweaks: conversation width');
|
|
584
|
+
// Mount / unmount tweak styles live as settings change. The subscribe
|
|
585
|
+
// callback re-runs on every settings change, so all toggles apply live.
|
|
586
|
+
// (Without `controller.subscribe(sync)` returning, sync only runs once at
|
|
587
|
+
// boot — a regression that would let tweak toggles never take effect after
|
|
588
|
+
// the first paint.)
|
|
589
|
+
ctx.effect(() => {
|
|
590
|
+
const cleanups = [];
|
|
591
|
+
const sync = () => {
|
|
592
|
+
const value = controller.getSnapshot().value;
|
|
593
|
+
const resolved = resolveValue(value);
|
|
594
|
+
// Full remount: simplest + tweak count is small (< 10).
|
|
595
|
+
while (cleanups.length > 0)
|
|
596
|
+
cleanups.pop()();
|
|
597
|
+
for (const tweak of registry_ts_1.TWEAKS) {
|
|
598
|
+
const enabled = resolved[tweak.settingKey] ?? tweak.defaultEnabled;
|
|
599
|
+
if (!enabled)
|
|
600
|
+
continue;
|
|
601
|
+
const injector = TWEAK_INJECTORS[tweak.id];
|
|
602
|
+
if (injector === undefined)
|
|
603
|
+
continue;
|
|
604
|
+
cleanups.push(injector(ctx));
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
sync();
|
|
608
|
+
return controller.subscribe(sync);
|
|
609
|
+
}, 'dsh-style-tweaks: live tweak styles');
|
|
610
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
611
|
+
name: 'settings.section',
|
|
612
|
+
id: NS,
|
|
613
|
+
order: 90,
|
|
614
|
+
label: () => t('nav'),
|
|
615
|
+
inject: () => ({ controller, t }),
|
|
616
|
+
}, SettingsSection));
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
__modules["./tweak-config.js"] = function(module, exports, require, __load_) {
|
|
620
|
+
"use strict";
|
|
621
|
+
/**
|
|
622
|
+
* dsh-style-tweaks — client-side constants & resolvers.
|
|
623
|
+
*
|
|
624
|
+
* Mirror of the runtime values in `src/config.ts`. Duplicated because the
|
|
625
|
+
* client tsconfig has `rootDir: src/client` (and explicitly excludes parent
|
|
626
|
+
* dirs), so a runtime `import` from `../config` is a compile-time error.
|
|
627
|
+
* Keep these in lock-step with `src/config.ts` — the server's schemastery
|
|
628
|
+
* `Config` object is the source of truth for defaults / clamps.
|
|
629
|
+
*
|
|
630
|
+
* Types are still pulled from `src/config.ts` via `import type`, which the
|
|
631
|
+
* rootDir restriction allows (no runtime reference is emitted).
|
|
632
|
+
*/
|
|
633
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
634
|
+
exports.DEFAULT_SETTINGS_NAV_SCROLL = exports.DEFAULT_LOCATE_CURRENT_SESSION = exports.DEFAULT_PROJECT_RUNNING_INDICATOR = exports.DEFAULT_CODE_BLOCK_FLUSH_TOP = exports.DEFAULT_STABLE_TURN_RAIL = exports.DEFAULT_STABLE_TABLE = exports.CONVERSATION_WIDTH_STORAGE_KEY = exports.MIN_SIDE_MARGIN = exports.DEFAULT_SIDE_MARGIN = exports.DEFAULT_USE_PLUGIN_WIDTH = exports.DEFAULT_DIALOG_WIDTH = exports.MAX_DIALOG_WIDTH = exports.MIN_DIALOG_WIDTH = void 0;
|
|
635
|
+
exports.resolveDialogWidth = resolveDialogWidth;
|
|
636
|
+
exports.resolveSideMargin = resolveSideMargin;
|
|
637
|
+
exports.resolveClientConfig = resolveClientConfig;
|
|
638
|
+
// ── Column-width constants (mirror src/config.ts) ───────────────────────
|
|
639
|
+
/** Dialog width min px. */
|
|
640
|
+
exports.MIN_DIALOG_WIDTH = 600;
|
|
641
|
+
/** Dialog width max px. */
|
|
642
|
+
exports.MAX_DIALOG_WIDTH = 1600;
|
|
643
|
+
/** Stock DSH column width. */
|
|
644
|
+
exports.DEFAULT_DIALOG_WIDTH = 748;
|
|
645
|
+
/** Plugin width control default (must match server; see config.ts). */
|
|
646
|
+
exports.DEFAULT_USE_PLUGIN_WIDTH = true;
|
|
647
|
+
/** Default side margin in px. */
|
|
648
|
+
exports.DEFAULT_SIDE_MARGIN = 50;
|
|
649
|
+
/** Minimum side margin in px. */
|
|
650
|
+
exports.MIN_SIDE_MARGIN = 32;
|
|
651
|
+
/** localStorage slot the native handle reads. */
|
|
652
|
+
exports.CONVERSATION_WIDTH_STORAGE_KEY = 'dsh.conversation.contentWidth';
|
|
653
|
+
// ── CSS tweak constants (mirror src/config.ts) ──────────────────────────
|
|
654
|
+
/** Default state of the stable-table tweak. */
|
|
655
|
+
exports.DEFAULT_STABLE_TABLE = true;
|
|
656
|
+
/** Default state of the stable-turn-rail tweak. */
|
|
657
|
+
exports.DEFAULT_STABLE_TURN_RAIL = true;
|
|
658
|
+
/** Default state of the code-block-flush-top tweak. */
|
|
659
|
+
exports.DEFAULT_CODE_BLOCK_FLUSH_TOP = true;
|
|
660
|
+
/** Default state of the project-running-indicator tweak. */
|
|
661
|
+
exports.DEFAULT_PROJECT_RUNNING_INDICATOR = true;
|
|
662
|
+
/** Default state of the locate-current-session tweak. */
|
|
663
|
+
exports.DEFAULT_LOCATE_CURRENT_SESSION = true;
|
|
664
|
+
/** Default state of the settings-nav-scroll tweak. */
|
|
665
|
+
exports.DEFAULT_SETTINGS_NAV_SCROLL = true;
|
|
666
|
+
/**
|
|
667
|
+
* Normalize a dialog width value (legacy strings included) to px.
|
|
668
|
+
* Must match `resolveDialogWidth` in src/config.ts.
|
|
669
|
+
*/
|
|
670
|
+
function resolveDialogWidth(value) {
|
|
671
|
+
if (typeof value === 'number') {
|
|
672
|
+
return Math.min(exports.MAX_DIALOG_WIDTH, Math.max(exports.MIN_DIALOG_WIDTH, Math.round(value)));
|
|
673
|
+
}
|
|
674
|
+
return exports.DEFAULT_DIALOG_WIDTH;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Normalize a side-margin value (legacy strings included) to px.
|
|
678
|
+
* Must match `resolveSideMargin` in src/config.ts.
|
|
679
|
+
*/
|
|
680
|
+
function resolveSideMargin(value) {
|
|
681
|
+
if (typeof value === 'number')
|
|
682
|
+
return Math.max(exports.MIN_SIDE_MARGIN, Math.round(value));
|
|
683
|
+
return exports.DEFAULT_SIDE_MARGIN;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Build a fully-defaulted ResolvedStyleTweaksConfig from any
|
|
687
|
+
* partial input. Mirror of `resolveConfig` in src/config.ts.
|
|
688
|
+
*/
|
|
689
|
+
function resolveClientConfig(value) {
|
|
690
|
+
return {
|
|
691
|
+
dialogWidth: resolveDialogWidth(value?.dialogWidth),
|
|
692
|
+
usePluginWidth: value?.usePluginWidth ?? exports.DEFAULT_USE_PLUGIN_WIDTH,
|
|
693
|
+
sideMargin: resolveSideMargin(value?.sideMargin),
|
|
694
|
+
stableTable: value?.stableTable ?? exports.DEFAULT_STABLE_TABLE,
|
|
695
|
+
stableTurnRail: value?.stableTurnRail ?? exports.DEFAULT_STABLE_TURN_RAIL,
|
|
696
|
+
codeBlockFlushTop: value?.codeBlockFlushTop ?? exports.DEFAULT_CODE_BLOCK_FLUSH_TOP,
|
|
697
|
+
projectRunningIndicator: value?.projectRunningIndicator ?? exports.DEFAULT_PROJECT_RUNNING_INDICATOR,
|
|
698
|
+
locateCurrentSession: value?.locateCurrentSession ?? exports.DEFAULT_LOCATE_CURRENT_SESSION,
|
|
699
|
+
settingsNavScroll: value?.settingsNavScroll ?? exports.DEFAULT_SETTINGS_NAV_SCROLL,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
__modules["./tweak-types.js"] = function(module, exports, require, __load_) {
|
|
704
|
+
"use strict";
|
|
705
|
+
/**
|
|
706
|
+
* dsh-style-tweaks — client-side type mirror.
|
|
707
|
+
*
|
|
708
|
+
* Mirror of the type definitions in `src/config.ts`. Duplicated because the
|
|
709
|
+
* client tsconfig has `rootDir: src/client`, so even `import type` from a
|
|
710
|
+
* parent directory is rejected by TS6059 ("file is not under rootDir").
|
|
711
|
+
* Keep these in lock-step with `src/config.ts`.
|
|
712
|
+
*
|
|
713
|
+
* The runtime values (constants, resolvers) live in `tweak-config.ts`.
|
|
714
|
+
*/
|
|
715
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
716
|
+
};
|
|
717
|
+
__modules["./tweaks/code-block-flush-top.js"] = function(module, exports, require, __load_) {
|
|
718
|
+
"use strict";
|
|
719
|
+
/**
|
|
720
|
+
* DSH-style-tweaks — code-block-flush-top tweak.
|
|
721
|
+
*
|
|
722
|
+
* Removes the 16 px of top whitespace above and inside every highlighted
|
|
723
|
+
* code block so the highlighted box sits flush with the preceding
|
|
724
|
+
* paragraph, list item, or heading *and* the first line of code sits flush
|
|
725
|
+
* with the top edge of the box. The right, bottom, and left sides keep
|
|
726
|
+
* 16 px so a code block still breathes inside its parent column and the
|
|
727
|
+
* code itself still has lateral breathing room.
|
|
728
|
+
*
|
|
729
|
+
* ## Symptom
|
|
730
|
+
*
|
|
731
|
+
* DSH renders every markdown code fence through `CodeBlock`
|
|
732
|
+
* (`packages/client/ui-primitives/src/markdown/CodeBlock.tsx:60`), which
|
|
733
|
+
* wraps the highlighted `<pre>` in a banner + `<pre>` pair:
|
|
734
|
+
*
|
|
735
|
+
* <div className={clsx(css.block, 'md-code-block', className)}>
|
|
736
|
+
* <div className={css.bannerWrap}> ... language / copy button ... </div>
|
|
737
|
+
* <pre className="shiki css-variables"> ... highlighted code ... </pre>
|
|
738
|
+
* </div>
|
|
739
|
+
*
|
|
740
|
+
* Two 16 px gaps stack vertically to create the "blank space above the
|
|
741
|
+
* code" the user sees:
|
|
742
|
+
*
|
|
743
|
+
* 1. The wrapper `.block` carries `margin: 16px 0` (DSH's
|
|
744
|
+
* `CodeBlock.module.css:13`), so the highlighted code is offset 16 px
|
|
745
|
+
* from preceding content (paragraph, list item, heading, ...).
|
|
746
|
+
* 2. The inner `<pre>` carries `padding: 16px` (CodeBlock.module.css:73),
|
|
747
|
+
* so the first line of code sits 16 px below the banner / box edge.
|
|
748
|
+
*
|
|
749
|
+
* Chrome DevTools exposes these on two different elements — clicking on the
|
|
750
|
+
* `<pre class="shiki css-variables">` shows `padding: 16px`, clicking on
|
|
751
|
+
* the `<div class="_block_xxxxx md-code-block">` wrapper shows
|
|
752
|
+
* `margin: 16px`. Both need to go for the code to sit flush.
|
|
753
|
+
*
|
|
754
|
+
* ## Fix
|
|
755
|
+
*
|
|
756
|
+
* Two rules on the stable `.md-code-block` hook (the same hand-written class
|
|
757
|
+
* `stable-table.ts` uses for `.md-table-wide`):
|
|
758
|
+
*
|
|
759
|
+
* - `.md-code-block { margin-top: 0 !important }` collapses the outer
|
|
760
|
+
* gap above the box. Only `margin-top` is touched: writing a full
|
|
761
|
+
* `margin: 0 16px 16px 16px` would add 16 px of left/right margin to
|
|
762
|
+
* the wrapper, shrinking the visible code box by 32 px (a regression
|
|
763
|
+
* reported by the user as "the box shrinks by a ring"). The wrapper's
|
|
764
|
+
* horizontal sizing is owned by the conversation column — there is no
|
|
765
|
+
* left/right margin on `.block` to preserve.
|
|
766
|
+
* - `.md-code-block pre { padding: 0 16px 16px 16px !important }`
|
|
767
|
+
* collapses the inner gap above the code. The streaming arm of
|
|
768
|
+
* `CodeBlock.tsx:130` mounts `<pre>` as a direct child of the wrapper;
|
|
769
|
+
* the settled arm (`CodeBlock.tsx:166`) wraps it in an anonymous
|
|
770
|
+
* `<div>` for the `dangerouslySetInnerHTML` sink. A descendant selector
|
|
771
|
+
* matches both arms identically.
|
|
772
|
+
*
|
|
773
|
+
* ## Specificity
|
|
774
|
+
*
|
|
775
|
+
* DSH's rules in play:
|
|
776
|
+
*
|
|
777
|
+
* .block { margin: 16px 0 } (0,0,1,0)
|
|
778
|
+
* .block:not(:last-child) { margin-bottom: 11px } (0,0,2,0)
|
|
779
|
+
* .block :where(pre) { padding: 16px } (0,0,2,0)
|
|
780
|
+
*
|
|
781
|
+
* A plain `.md-code-block { ... !important }` is (0,0,1,0) and would lose
|
|
782
|
+
* to `.block:not(:last-child)` on the trailing-edge specificity tiebreak
|
|
783
|
+
* (the :not() pseudo-class adds one to specificity). Doubling the class —
|
|
784
|
+
* .md-code-block.md-code-block — reaches (0,0,2,0) and ties; !important
|
|
785
|
+
* then takes precedence on importance, leaving us in control of all four
|
|
786
|
+
* edges.
|
|
787
|
+
*
|
|
788
|
+
* The inner-pre selector uses .md-code-block.md-code-block pre, which is
|
|
789
|
+
* (0,0,2,1) — that strictly beats DSH's .block :where(pre) (0,0,2,0) on
|
|
790
|
+
* total specificity. The !important is belt-and-suspenders, but the
|
|
791
|
+
* specificity win alone is enough.
|
|
792
|
+
*
|
|
793
|
+
* ## Scope safety
|
|
794
|
+
*
|
|
795
|
+
* `.md-code-block` is set in exactly one place in DSH's source
|
|
796
|
+
* (`CodeBlock.tsx:170`); DSH reserves it for plugins/extensions so future
|
|
797
|
+
* code does not collide. The empty-fence branch (`renderCode` in
|
|
798
|
+
* `packages/client/ui-primitives/src/markdown/render.tsx:312-321`) renders a
|
|
799
|
+
* bare `<pre>` without `.md-code-block`, so empty fences are untouched —
|
|
800
|
+
* they have no banner/wrapper anyway.
|
|
801
|
+
*
|
|
802
|
+
* Streaming and settled arms of `CodeBlock` (`CodeBlock.tsx:130` streaming
|
|
803
|
+
* `<pre>`, `:166` settled `<div dangerouslySetInnerHTML>`) share the same
|
|
804
|
+
* wrapper class, so both arms are affected uniformly by the descendant
|
|
805
|
+
* selector.
|
|
806
|
+
*/
|
|
807
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
808
|
+
exports.CODE_BLOCK_FLUSH_TOP_CSS_ID = void 0;
|
|
809
|
+
exports.injectCodeBlockFlushTopStyles = injectCodeBlockFlushTopStyles;
|
|
810
|
+
const CODE_BLOCK_FLUSH_TOP_CSS = `
|
|
811
|
+
/* Outer gap above the highlighted box: DSH's wrapper rule (.block { margin:
|
|
812
|
+
* 16px 0 }, CodeBlock.module.css:13) sets the wrapper's top/bottom margin
|
|
813
|
+
* via a single-class selector hashed by CSS Modules (specificity 0,0,1,0).
|
|
814
|
+
* DSH also tightens the trailing edge with .block:not(:last-child) {
|
|
815
|
+
* margin-bottom: 11px } — that one has specificity 0,0,2,0. We double-up
|
|
816
|
+
* .md-code-block (the stable hand-written hook reserved for plugins,
|
|
817
|
+
* CodeBlock.tsx:170) to reach 0,0,2,0 + !important so we beat the
|
|
818
|
+
* trailing-edge tightening on the importance tiebreak.
|
|
819
|
+
*
|
|
820
|
+
* Only margin-top is reset: writing margin: 0 16px 16px 16px !important
|
|
821
|
+
* would add 16 px of left/right margin to the wrapper, shrinking the
|
|
822
|
+
* visible code box by 32 px (the user reported the box "shrinks by a
|
|
823
|
+
* ring"). Setting just margin-top: 0 !important collapses the gap above
|
|
824
|
+
* the box while preserving DSH's natural horizontal margin (zero on the
|
|
825
|
+
* wrapper itself — its width comes from the conversation column). */
|
|
826
|
+
.md-code-block,
|
|
827
|
+
.md-code-block.md-code-block {
|
|
828
|
+
margin-top: 0 !important;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* Inner gap above the highlighted code itself: DSH's .block :where(pre)
|
|
832
|
+
* rule sets padding: 16px on the pre element (CodeBlock.module.css:71-74).
|
|
833
|
+
* That padding puts the first line of code 16px below the banner / box
|
|
834
|
+
* edge, which reads as "extra blank space inside the box above the code".
|
|
835
|
+
* We collapse only the top edge so the code still breathes laterally.
|
|
836
|
+
*
|
|
837
|
+
* Specificity note: DSH's selector is .block :where(pre) which is
|
|
838
|
+
* (0,0,2,0) (one class + the :where() pseudo-class contributes nothing).
|
|
839
|
+
* A plain descendant selector .md-code-block pre is (0,0,1,1) and DSH
|
|
840
|
+
* still wins on total specificity. So we double-up the class:
|
|
841
|
+
* .md-code-block.md-code-block pre is (0,0,2,1) and beats DSH.
|
|
842
|
+
*
|
|
843
|
+
* The streaming arm of CodeBlock (line 130) mounts pre as a direct child
|
|
844
|
+
* of .md-code-block; the settled arm (line 166) wraps the pre in an extra
|
|
845
|
+
* anonymous div for the dangerouslySetInnerHTML sink. Descendant selection
|
|
846
|
+
* matches both arms identically. */
|
|
847
|
+
.md-code-block.md-code-block pre,
|
|
848
|
+
.md-code-block.md-code-block pre.shiki {
|
|
849
|
+
padding: 0 16px 16px 16px !important;
|
|
850
|
+
}
|
|
851
|
+
`;
|
|
852
|
+
exports.CODE_BLOCK_FLUSH_TOP_CSS_ID = 'cst-code-block-flush-top';
|
|
853
|
+
function injectCodeBlockFlushTopStyles() {
|
|
854
|
+
let style = document.querySelector(`style[data-tweak-css="${exports.CODE_BLOCK_FLUSH_TOP_CSS_ID}"]`);
|
|
855
|
+
if (style === null) {
|
|
856
|
+
style = document.createElement('style');
|
|
857
|
+
style.dataset.tweak = 'cst';
|
|
858
|
+
style.dataset.tweakCss = exports.CODE_BLOCK_FLUSH_TOP_CSS_ID;
|
|
859
|
+
style.textContent = CODE_BLOCK_FLUSH_TOP_CSS;
|
|
860
|
+
document.head.appendChild(style);
|
|
861
|
+
}
|
|
862
|
+
return () => { style?.remove(); };
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
__modules["./tweaks/locate-current-session.js"] = function(module, exports, require, __load_) {
|
|
866
|
+
"use strict";
|
|
867
|
+
/**
|
|
868
|
+
* dsh-style-tweaks — locate-current-session tweak.
|
|
869
|
+
*
|
|
870
|
+
* Adds a "locate current session" button to the left of the native search
|
|
871
|
+
* button in the sidebar's "工作区" (workspaces) section header. Clicking it:
|
|
872
|
+
*
|
|
873
|
+
* 1. Finds the currently-selected session row in the sidebar tree.
|
|
874
|
+
* 2. Locates its enclosing workspace group via the stable
|
|
875
|
+
* `[class*="groupSection"]` ancestor hook.
|
|
876
|
+
* 3. If the parent workspace row is collapsed (`aria-expanded="false"`),
|
|
877
|
+
* clicks it to expand.
|
|
878
|
+
* 4. Scrolls the session row to the center of the sidebar's visible area
|
|
879
|
+
* so the user can confirm "this is where I am".
|
|
880
|
+
*
|
|
881
|
+
* ## Why DOM patching
|
|
882
|
+
*
|
|
883
|
+
* DSH's `sidebar.workspaces` slot is a single owner (the workspace browser)
|
|
884
|
+
* and exposes no public sub-slot for the section header row. There is also
|
|
885
|
+
* no plugin-callable service that scrolls a sidebar row into view. The
|
|
886
|
+
* plugin therefore decorates the DOM from outside, exactly the way
|
|
887
|
+
* `project-running-indicator.ts` decorates project header rows.
|
|
888
|
+
*
|
|
889
|
+
* Stable anchors (CSS-Modules hashes rotate per build):
|
|
890
|
+
* • Native search button: localized `aria-label` resolved through the
|
|
891
|
+
* locale service (`workspace` ns, key `search.sessions.aria`), with a
|
|
892
|
+
* structural fallback on the `searchButton` class fragment.
|
|
893
|
+
* • Breadcrumb: same strategy (`conversation` ns, key
|
|
894
|
+
* `session.hierarchy`; fallback `crumbs` fragment).
|
|
895
|
+
* • Selected session row: `[role="treeitem"][aria-selected="true"]`
|
|
896
|
+
* (ARIA `aria-selected` is part of the WAI-ARIA tree pattern, DSH
|
|
897
|
+
* uses it to mark the active row.)
|
|
898
|
+
* • Workspace group container: closest element whose class attribute
|
|
899
|
+
* contains the substring `groupSection` (a semantic fragment DSH
|
|
900
|
+
* keeps even when its hashed prefix changes).
|
|
901
|
+
* • Workspace header row: `[role="treeitem"][aria-expanded]` inside the
|
|
902
|
+
* same group container.
|
|
903
|
+
*
|
|
904
|
+
* ## Lifecycle
|
|
905
|
+
*
|
|
906
|
+
* `setupLocateCurrentSession` returns a disposer that removes the button
|
|
907
|
+
* and disconnects the observer. The caller in `index.tsx` mounts/unmounts
|
|
908
|
+
* it on every settings change, exactly like the other JS-level tweaks —
|
|
909
|
+
* disabling the toggle in Settings causes the button to disappear with no
|
|
910
|
+
* page refresh.
|
|
911
|
+
*
|
|
912
|
+
* @module dsh-style-tweaks/client/tweaks/locate-current-session
|
|
913
|
+
*/
|
|
914
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
915
|
+
exports.injectLocateCurrentSessionStyles = injectLocateCurrentSessionStyles;
|
|
916
|
+
exports.setupLocateCurrentSession = setupLocateCurrentSession;
|
|
917
|
+
/** Stable hook for the currently selected session row. */
|
|
918
|
+
const SELECTED_SESSION_SELECTOR = '[role="treeitem"][aria-selected="true"]';
|
|
919
|
+
/**
|
|
920
|
+
* Class fragment selector for the workspace group container. DSH marks
|
|
921
|
+
* each workspace's group block with a class whose name contains
|
|
922
|
+
* `groupSection`; the rest of the class name is a CSS-Modules hash.
|
|
923
|
+
* Attribute-substring matching is more durable than a fixed-class match.
|
|
924
|
+
*/
|
|
925
|
+
const GROUP_SELECTOR = '[class*="groupSection"]';
|
|
926
|
+
/** Stable hook for a workspace header row inside the group. */
|
|
927
|
+
const PROJECT_ROW_SELECTOR = '[role="treeitem"][aria-expanded]';
|
|
928
|
+
let tWorkspace;
|
|
929
|
+
let tConversation;
|
|
930
|
+
/**
|
|
931
|
+
* The localized aria-label DSH paints on the sidebar search button
|
|
932
|
+
* (namespace `workspace`, key `search.sessions.aria`: zh `搜索会话`,
|
|
933
|
+
* en `Search sessions`).
|
|
934
|
+
*/
|
|
935
|
+
function searchButtonLabel() {
|
|
936
|
+
return tWorkspace?.('search.sessions.aria');
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* The localized aria-label of the breadcrumb nav that carries the current
|
|
940
|
+
* session title (namespace `conversation`, key `session.hierarchy`).
|
|
941
|
+
*/
|
|
942
|
+
function breadcrumbLabel() {
|
|
943
|
+
return tConversation?.('session.hierarchy');
|
|
944
|
+
}
|
|
945
|
+
/** Stable structural fallback for the sidebar search button. CSS-Modules
|
|
946
|
+
* class names are `<hash>_<semantic>`; the semantic suffix survives builds,
|
|
947
|
+
* so a substring match is build-durable where the full class is not. */
|
|
948
|
+
const SEARCH_BUTTON_FALLBACK_SELECTOR = 'button[class*="searchButton"]';
|
|
949
|
+
/** Stable structural fallback for the breadcrumb nav (`css.crumbs`). */
|
|
950
|
+
const BREADCRUMB_FALLBACK_SELECTOR = 'nav[class*="crumbs"] button[disabled]';
|
|
951
|
+
/**
|
|
952
|
+
* Inline SVG icon for the locate button.
|
|
953
|
+
*
|
|
954
|
+
* Inline SVG icon for the locate button.
|
|
955
|
+
*
|
|
956
|
+
* A stroke-based target / crosshair glyph drawn in a 16×16 viewBox to
|
|
957
|
+
* match DSH's native icon coordinate system. Stroke width is the
|
|
958
|
+
* user-tuned 1.1 — thin enough to feel precise, thick enough to read
|
|
959
|
+
* at the 16px display size. `currentColor` lets the button recolor on
|
|
960
|
+
* hover like the natives.
|
|
961
|
+
*/
|
|
962
|
+
const PIN_ICON_SVG = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" fill="none"/><path d="M8 1v3M8 12v3M1 8h3M12 8h3" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" fill="none"/><circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none"/></svg>';
|
|
963
|
+
/** CSS injected once per tweak-enable cycle; lives until the cycle disables. */
|
|
964
|
+
const LOCATE_CSS_ID = 'cst-locate-current-session';
|
|
965
|
+
const LOCATE_CSS = `
|
|
966
|
+
/* Injected button: visually consistent with the native search button.
|
|
967
|
+
* Key styles matched from DSH's native icon buttons:
|
|
968
|
+
* - corner-shape: superellipse(1) — creates the smooth circular hover
|
|
969
|
+
* background (border-radius alone is not enough)
|
|
970
|
+
* - display: flex — native buttons use flex, not grid
|
|
971
|
+
* - Spacing comes from the search slot's flex gap (see the slot rule
|
|
972
|
+
* below), matching the section header's native 4px icon gap */
|
|
973
|
+
button.cst-locate-btn {
|
|
974
|
+
flex: none;
|
|
975
|
+
display: flex;
|
|
976
|
+
place-items: center;
|
|
977
|
+
align-items: center;
|
|
978
|
+
justify-content: center;
|
|
979
|
+
width: 28px;
|
|
980
|
+
height: 28px;
|
|
981
|
+
padding: 0;
|
|
982
|
+
border: none;
|
|
983
|
+
border-radius: 50%;
|
|
984
|
+
corner-shape: superellipse(1);
|
|
985
|
+
background: transparent;
|
|
986
|
+
color: var(--dsw-alias-label-secondary, currentColor);
|
|
987
|
+
cursor: pointer;
|
|
988
|
+
position: relative;
|
|
989
|
+
z-index: 0;
|
|
990
|
+
transition: background-color .15s ease, color .15s ease;
|
|
991
|
+
}
|
|
992
|
+
button.cst-locate-btn > svg {
|
|
993
|
+
width: 16px;
|
|
994
|
+
height: 16px;
|
|
995
|
+
display: block;
|
|
996
|
+
}
|
|
997
|
+
button.cst-locate-btn:hover:not(:disabled) {
|
|
998
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06));
|
|
999
|
+
color: var(--dsw-alias-label-primary, currentColor);
|
|
1000
|
+
}
|
|
1001
|
+
button.cst-locate-btn:disabled {
|
|
1002
|
+
opacity: .35;
|
|
1003
|
+
cursor: default;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* Hover tooltip bubble, mirrored from DSH's own Tooltip primitive
|
|
1007
|
+
* (ui-primitives/src/Tooltip.module.css): fixed-position span, theme
|
|
1008
|
+
* tokens, 150ms fade-in. Kept as plain CSS so the injected button —
|
|
1009
|
+
* which lives outside React — renders the exact same visual. */
|
|
1010
|
+
.cst-locate-tooltip {
|
|
1011
|
+
position: fixed;
|
|
1012
|
+
z-index: 100;
|
|
1013
|
+
width: max-content;
|
|
1014
|
+
max-width: 50vw;
|
|
1015
|
+
padding: 3px 7px;
|
|
1016
|
+
border-radius: 8px;
|
|
1017
|
+
background: var(--dsw-alias-tooltip-bg, rgba(0, 0, 0, 0.85));
|
|
1018
|
+
color: var(--dsw-static-neutral-bluish-00, #fff);
|
|
1019
|
+
font-size: 13px;
|
|
1020
|
+
line-height: 20px;
|
|
1021
|
+
white-space: pre-line;
|
|
1022
|
+
overflow-wrap: break-word;
|
|
1023
|
+
pointer-events: none;
|
|
1024
|
+
animation: cst-tooltip-in 150ms var(--ds-ease-in-out, ease-in-out);
|
|
1025
|
+
}
|
|
1026
|
+
.cst-locate-tooltip[data-side='bottom'] {
|
|
1027
|
+
transform: translateX(-50%);
|
|
1028
|
+
}
|
|
1029
|
+
.cst-locate-tooltip[data-side='top'] {
|
|
1030
|
+
transform: translate(-50%, -100%);
|
|
1031
|
+
}
|
|
1032
|
+
@keyframes cst-tooltip-in {
|
|
1033
|
+
from { opacity: 0; }
|
|
1034
|
+
}
|
|
1035
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1036
|
+
.cst-locate-tooltip {
|
|
1037
|
+
animation: none;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/* The locate button is mounted as a left sibling of the search container
|
|
1041
|
+
* inside the search slot. Force the slot into a right-aligned flex row
|
|
1042
|
+
* with the same 4px gap the section header uses, so all four icons
|
|
1043
|
+
* (locate / search / view options / add workspace) are evenly spaced.
|
|
1044
|
+
* justify-content: flex-end keeps the group pinned to the right edge
|
|
1045
|
+
* exactly as DSH's native layout does.
|
|
1046
|
+
*
|
|
1047
|
+
* Anchors use semantic CSS-Modules fragments ("searchSlot" survives as a
|
|
1048
|
+
* suffix after the per-build hash prefix), never full hashed names. */
|
|
1049
|
+
div[class*="searchSlot"]:has(> button.cst-locate-btn) {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
align-items: center;
|
|
1052
|
+
justify-content: flex-end;
|
|
1053
|
+
gap: 4px;
|
|
1054
|
+
}
|
|
1055
|
+
/* The search container directly follows our button in the slot. */
|
|
1056
|
+
button.cst-locate-btn + * {
|
|
1057
|
+
flex: none;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
`;
|
|
1061
|
+
/**
|
|
1062
|
+
* Install the locate-current-session stylesheet into <head>. Idempotent: a
|
|
1063
|
+
* second call returns no-op (the existing <style> is reused). The caller
|
|
1064
|
+
* keeps the disposer alive while the tweak is enabled and disposes it when
|
|
1065
|
+
* the user disables the tweak.
|
|
1066
|
+
*/
|
|
1067
|
+
function injectLocateCurrentSessionStyles() {
|
|
1068
|
+
let style = document.querySelector(`style[data-tweak-css="${LOCATE_CSS_ID}"]`);
|
|
1069
|
+
if (style === null) {
|
|
1070
|
+
style = document.createElement('style');
|
|
1071
|
+
style.dataset.tweak = 'cst';
|
|
1072
|
+
style.dataset.tweakCss = LOCATE_CSS_ID;
|
|
1073
|
+
style.textContent = LOCATE_CSS;
|
|
1074
|
+
document.head.appendChild(style);
|
|
1075
|
+
}
|
|
1076
|
+
return () => { style?.remove(); };
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Read the current session's title from the top breadcrumb. DSH renders
|
|
1080
|
+
* the session lineage chip as a nav whose aria-label comes from the
|
|
1081
|
+
* locale service (`conversation` ns, key `session.hierarchy`); the current
|
|
1082
|
+
* session's button is the only `button[disabled]` inside it (DSH disables
|
|
1083
|
+
* the current crumb so it cannot be re-entered as a navigation target).
|
|
1084
|
+
* Returns the trimmed title text, or null when no session is open.
|
|
1085
|
+
*/
|
|
1086
|
+
function readCurrentSessionTitle() {
|
|
1087
|
+
const label = breadcrumbLabel();
|
|
1088
|
+
const selector = label !== undefined
|
|
1089
|
+
? `nav[aria-label="${label}"] button[disabled]`
|
|
1090
|
+
: BREADCRUMB_FALLBACK_SELECTOR;
|
|
1091
|
+
const btn = document.querySelector(selector);
|
|
1092
|
+
const text = btn?.textContent?.trim();
|
|
1093
|
+
return text !== undefined && text.length > 0 ? text : null;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Find the sidebar treeitem that hosts the given session title. Walks every
|
|
1097
|
+
* `[class*="groupSection"]` in the sidebar and, for each one that is
|
|
1098
|
+
* currently expanded, looks for a session treeitem whose textContent starts
|
|
1099
|
+
* with `title` (titles are unique per workspace, so prefix match is enough
|
|
1100
|
+
* — sidebar rows also include trailing "1天" / "20小时" labels).
|
|
1101
|
+
*
|
|
1102
|
+
* Returns null when no match is found (e.g. the workspace is currently
|
|
1103
|
+
* collapsed, hiding its session rows). The caller falls back to the title
|
|
1104
|
+
* itself for the workspace lookup.
|
|
1105
|
+
*/
|
|
1106
|
+
function findSessionRow(title) {
|
|
1107
|
+
for (const group of document.querySelectorAll(GROUP_SELECTOR)) {
|
|
1108
|
+
const projectRow = group.querySelector(PROJECT_ROW_SELECTOR);
|
|
1109
|
+
if (projectRow?.getAttribute('aria-expanded') !== 'true')
|
|
1110
|
+
continue;
|
|
1111
|
+
for (const session of group.querySelectorAll('[role="treeitem"]:not([aria-expanded])')) {
|
|
1112
|
+
if ((session.textContent ?? '').trimStart().startsWith(title)) {
|
|
1113
|
+
return session;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Find the workspace projectRow (and its enclosing groupSection) that
|
|
1121
|
+
* contains the given session title. First tries an exact session-row match
|
|
1122
|
+
* in expanded groups; if none, matches by workspace title text using the
|
|
1123
|
+
* hint derived from the current session title (we use the current title
|
|
1124
|
+
* itself as a last-ditch hint — DSH sidebar group headers don't carry
|
|
1125
|
+
* their workspace id as a stable attribute, so workspace title matching is
|
|
1126
|
+
* the only DOM-resident anchor we have).
|
|
1127
|
+
*/
|
|
1128
|
+
/**
|
|
1129
|
+
* Resolve the workspace title for the current session via the app stores.
|
|
1130
|
+
* `ctx.get('sessions').list.byId` maps id→{title}; `ctx.get('workspaces')
|
|
1131
|
+
* .list.items[*].sessionIds` lists which sessions live in each workspace.
|
|
1132
|
+
* Returns the workspace title (the same string DSH renders into the
|
|
1133
|
+
* sidebar project row), or null when either store is unavailable, the
|
|
1134
|
+
* session is not present in any workspace (ungrouped / archived), or the
|
|
1135
|
+
* breadcrumb title does not match any known session.
|
|
1136
|
+
*/
|
|
1137
|
+
function resolveWorkspaceTitle(title, sessionList, workspaceList) {
|
|
1138
|
+
if (sessionList === undefined || workspaceList === undefined)
|
|
1139
|
+
return null;
|
|
1140
|
+
const sessions = sessionList.getSnapshot();
|
|
1141
|
+
let sessionId;
|
|
1142
|
+
for (const [id, summary] of Object.entries(sessions.byId)) {
|
|
1143
|
+
if (summary?.title === title) {
|
|
1144
|
+
sessionId = id;
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (sessionId === undefined)
|
|
1149
|
+
return null;
|
|
1150
|
+
for (const ws of workspaceList.getSnapshot().items) {
|
|
1151
|
+
if (ws.sessionIds.includes(sessionId))
|
|
1152
|
+
return ws.title;
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Find the sidebar projectRow whose text equals `workspaceTitle`. DSH
|
|
1158
|
+
* renders each workspace header as a treeitem whose textContent is the
|
|
1159
|
+
* raw title (verified live: "dsh-dialog-width" while collapsed, same when
|
|
1160
|
+
* expanded). Returns the treeitem + its enclosing groupSection for click.
|
|
1161
|
+
*/
|
|
1162
|
+
function findProjectRowByTitle(workspaceTitle) {
|
|
1163
|
+
for (const group of document.querySelectorAll(GROUP_SELECTOR)) {
|
|
1164
|
+
const projectRow = group.querySelector(PROJECT_ROW_SELECTOR);
|
|
1165
|
+
if (projectRow === null)
|
|
1166
|
+
continue;
|
|
1167
|
+
if ((projectRow.textContent ?? '').trim() === workspaceTitle) {
|
|
1168
|
+
return { group, projectRow };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Click a group's "expand remaining sessions" overflow button when DSH
|
|
1175
|
+
* caps the group's rendered session rows (default 5) and hides the rest
|
|
1176
|
+
* behind it. The button carries `aria-expanded` matching its state and a
|
|
1177
|
+
* semantic `sessionOverflowButton` class fragment that survives the
|
|
1178
|
+
* per-build hash prefix. No-op when the button is absent or already
|
|
1179
|
+
* expanded.
|
|
1180
|
+
*/
|
|
1181
|
+
function expandOverflow(group) {
|
|
1182
|
+
const btn = group.querySelector('button[class*="sessionOverflowButton"][aria-expanded="false"]');
|
|
1183
|
+
btn?.click();
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Perform the locate action. Steps:
|
|
1187
|
+
* 1. Read the current session title from the breadcrumb (the only DOM
|
|
1188
|
+
* anchor that survives a sidebar rebuild).
|
|
1189
|
+
* 2. Fast path: if the sidebar already has the selected session row,
|
|
1190
|
+
* scroll to it. This covers the common "all open" case in O(1).
|
|
1191
|
+
* 3. Slow path: resolve the session → workspace via the app stores,
|
|
1192
|
+
* find the workspace's projectRow in the sidebar, and click it open
|
|
1193
|
+
* so DSH mounts the session row on the next render.
|
|
1194
|
+
* 4. After the click, the new treeitem carries `aria-selected="true"`
|
|
1195
|
+
* (DSH marks the current session on every tree paint). One rAF is
|
|
1196
|
+
* enough for the synchronous DOM update; we then scroll it into view.
|
|
1197
|
+
*/
|
|
1198
|
+
function performLocate(sessionList, workspaceList) {
|
|
1199
|
+
const title = readCurrentSessionTitle();
|
|
1200
|
+
if (title === null)
|
|
1201
|
+
return;
|
|
1202
|
+
// Fast path: the selected session row is already mounted.
|
|
1203
|
+
let session = document.querySelector(SELECTED_SESSION_SELECTOR);
|
|
1204
|
+
if (session !== null && (session.textContent ?? '').trimStart().startsWith(title)) {
|
|
1205
|
+
session.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
// Slow path: the workspace is collapsed (the session row is not in the
|
|
1209
|
+
// DOM). Look up the workspace title from the app stores.
|
|
1210
|
+
const workspaceTitle = resolveWorkspaceTitle(title, sessionList, workspaceList);
|
|
1211
|
+
if (workspaceTitle === null) {
|
|
1212
|
+
// No store or no mapping — bail. The breadcrumb still tells the user
|
|
1213
|
+
// which session is current.
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const hit = findProjectRowByTitle(workspaceTitle);
|
|
1217
|
+
if (hit === null) {
|
|
1218
|
+
// The session belongs to a workspace that's not in the sidebar at all
|
|
1219
|
+
// (e.g. archived). Nothing to reveal.
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const { projectRow, group } = hit;
|
|
1223
|
+
const collapsed = projectRow.getAttribute('aria-expanded') === 'false';
|
|
1224
|
+
// Scroll the selected row into view once it exists; when it still doesn't
|
|
1225
|
+
// (the group caps rendered rows and the current session hides behind the
|
|
1226
|
+
// "expand remaining N sessions" overflow), click the overflow open first
|
|
1227
|
+
// and re-query on the next frame.
|
|
1228
|
+
const revealAndScroll = () => {
|
|
1229
|
+
const next = document.querySelector(SELECTED_SESSION_SELECTOR);
|
|
1230
|
+
if (next !== null) {
|
|
1231
|
+
next.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
expandOverflow(group);
|
|
1235
|
+
requestAnimationFrame(() => {
|
|
1236
|
+
document.querySelector(SELECTED_SESSION_SELECTOR)
|
|
1237
|
+
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1240
|
+
if (collapsed) {
|
|
1241
|
+
// Click triggers DSH's expand handler, which mounts the session rows
|
|
1242
|
+
// and marks the current one with `aria-selected="true"`.
|
|
1243
|
+
projectRow.click();
|
|
1244
|
+
requestAnimationFrame(revealAndScroll);
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
// Workspace open but the selected marker is missing (DSH state edge
|
|
1248
|
+
// case, or the row hides behind the overflow button): re-query by
|
|
1249
|
+
// title within the expanded group.
|
|
1250
|
+
session = findSessionRow(title);
|
|
1251
|
+
if (session !== null) {
|
|
1252
|
+
session.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
revealAndScroll();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
/** Build the button element. Reused across mount calls. The click
|
|
1260
|
+
* handler is wired separately in `setupLocateCurrentSession` so it can
|
|
1261
|
+
* capture the live session/workspace store references at click time.
|
|
1262
|
+
*
|
|
1263
|
+
* We use the `title` attribute for the tooltip — this triggers the
|
|
1264
|
+
* browser-native tooltip which has the same black background as the
|
|
1265
|
+
* tooltips on DSH's other native icon buttons (search, view options,
|
|
1266
|
+
* add workspace). This keeps the tooltip style consistent without
|
|
1267
|
+
* custom CSS. */
|
|
1268
|
+
function buildButton() {
|
|
1269
|
+
const btn = document.createElement('button');
|
|
1270
|
+
btn.type = 'button';
|
|
1271
|
+
btn.className = 'cst-locate-btn';
|
|
1272
|
+
btn.dataset.cstLocateBtn = '';
|
|
1273
|
+
const { label } = buttonCopy();
|
|
1274
|
+
btn.setAttribute('aria-label', label);
|
|
1275
|
+
// No `title` while enabled — the custom bubble below plays the native
|
|
1276
|
+
// Tooltip's role instead; a title would double up with it.
|
|
1277
|
+
btn.innerHTML = PIN_ICON_SVG;
|
|
1278
|
+
attachTooltip(btn);
|
|
1279
|
+
return btn;
|
|
1280
|
+
}
|
|
1281
|
+
/** Is the active DSH locale a Chinese one? Read from `<html lang>`, which
|
|
1282
|
+
* the locale service keeps synced to the active locale (`zh-CN`, `en`, …).
|
|
1283
|
+
* Used only for our own button copy — DSH's own strings go through `t`. */
|
|
1284
|
+
function isZhLocale() {
|
|
1285
|
+
return (document.documentElement.lang ?? '').toLowerCase().startsWith('zh');
|
|
1286
|
+
}
|
|
1287
|
+
/** Localized copy for the injected button (enabled + disabled states). */
|
|
1288
|
+
function buttonCopy() {
|
|
1289
|
+
return isZhLocale()
|
|
1290
|
+
? { label: '定位当前会话', disabled: '请先打开一个会话' }
|
|
1291
|
+
: { label: 'Locate current session', disabled: 'Open a session first' };
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Hover tooltip, replicating DSH's native `<Tooltip>` primitive for our
|
|
1295
|
+
* non-React button: 500ms hover delay, `position: fixed` bubble appended
|
|
1296
|
+
* to `<body>` (escapes ancestor overflow clipping without a portal),
|
|
1297
|
+
* bottom placement with the same viewport-fit moves — horizontal slide
|
|
1298
|
+
* back inside near the edges, vertical flip above when there is no room
|
|
1299
|
+
* below. The single live bubble lives in a module variable; the button is
|
|
1300
|
+
* remounted by DSH on view transitions, so a shared instance survives.
|
|
1301
|
+
*/
|
|
1302
|
+
const TOOLTIP_DELAY_MS = 500;
|
|
1303
|
+
const EDGE_MARGIN = 12;
|
|
1304
|
+
let liveBubble = null;
|
|
1305
|
+
function hideTooltip() {
|
|
1306
|
+
liveBubble?.remove();
|
|
1307
|
+
liveBubble = null;
|
|
1308
|
+
}
|
|
1309
|
+
function showTooltip(anchor, text) {
|
|
1310
|
+
hideTooltip();
|
|
1311
|
+
const r = anchor.getBoundingClientRect();
|
|
1312
|
+
const bubble = document.createElement('span');
|
|
1313
|
+
bubble.className = 'cst-locate-tooltip';
|
|
1314
|
+
bubble.setAttribute('role', 'tooltip');
|
|
1315
|
+
bubble.textContent = text;
|
|
1316
|
+
bubble.dataset.side = 'bottom';
|
|
1317
|
+
bubble.style.left = `${r.left + r.width / 2}px`;
|
|
1318
|
+
bubble.style.top = `${r.bottom + 8}px`;
|
|
1319
|
+
document.body.appendChild(bubble);
|
|
1320
|
+
liveBubble = bubble;
|
|
1321
|
+
// Viewport fit, same moves as DSH's fit pass.
|
|
1322
|
+
const b = bubble.getBoundingClientRect();
|
|
1323
|
+
let dx = 0;
|
|
1324
|
+
if (b.right > window.innerWidth - EDGE_MARGIN)
|
|
1325
|
+
dx = window.innerWidth - EDGE_MARGIN - b.right;
|
|
1326
|
+
if (b.left + dx < EDGE_MARGIN)
|
|
1327
|
+
dx = EDGE_MARGIN - b.left;
|
|
1328
|
+
bubble.style.left = `${r.left + r.width / 2 + dx}px`;
|
|
1329
|
+
const fitsBelow = r.bottom + 8 + b.height <= window.innerHeight - EDGE_MARGIN;
|
|
1330
|
+
const fitsAbove = r.top - 8 - b.height >= EDGE_MARGIN;
|
|
1331
|
+
if (!fitsBelow && fitsAbove) {
|
|
1332
|
+
bubble.dataset.side = 'top';
|
|
1333
|
+
bubble.style.top = `${r.top - 8}px`;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Wire the tooltip triggers on a mounted button, chaining the DSH
|
|
1338
|
+
* semantics: hover delayed, keyboard focus immediate, either leaving
|
|
1339
|
+
* hides. A disabled button fires no mouse events, so the disabled hint
|
|
1340
|
+
* rides the `title` attribute instead (browser-native, still works).
|
|
1341
|
+
*/
|
|
1342
|
+
function attachTooltip(btn) {
|
|
1343
|
+
let timer = null;
|
|
1344
|
+
const cancel = () => {
|
|
1345
|
+
if (timer !== null) {
|
|
1346
|
+
clearTimeout(timer);
|
|
1347
|
+
timer = null;
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
btn.addEventListener('mouseenter', () => {
|
|
1351
|
+
if (btn.disabled)
|
|
1352
|
+
return;
|
|
1353
|
+
cancel();
|
|
1354
|
+
timer = setTimeout(() => {
|
|
1355
|
+
timer = null;
|
|
1356
|
+
showTooltip(btn, buttonCopy().label);
|
|
1357
|
+
}, TOOLTIP_DELAY_MS);
|
|
1358
|
+
});
|
|
1359
|
+
btn.addEventListener('mouseleave', () => { cancel(); hideTooltip(); });
|
|
1360
|
+
btn.addEventListener('blur', () => { cancel(); hideTooltip(); });
|
|
1361
|
+
btn.addEventListener('focus', () => {
|
|
1362
|
+
if (btn.disabled)
|
|
1363
|
+
return;
|
|
1364
|
+
cancel();
|
|
1365
|
+
showTooltip(btn, buttonCopy().label);
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Reflect current-session availability on the injected button.
|
|
1370
|
+
* - Has a session → enabled, aria-label stays "定位当前会话".
|
|
1371
|
+
* - No session → disabled, aria-label becomes "请先打开一个会话".
|
|
1372
|
+
*
|
|
1373
|
+
* We swap the aria-label (not `title`) when disabled because:
|
|
1374
|
+
* - DSH's native icon buttons don't use `title` — they rely on
|
|
1375
|
+
* aria-label only, and any tooltip layer DSH paints is keyed off
|
|
1376
|
+
* aria-label.
|
|
1377
|
+
* - Setting `title` would summon the browser-native tooltip, which
|
|
1378
|
+
* has a different visual style from the rest of the sidebar.
|
|
1379
|
+
*
|
|
1380
|
+
* The "has session" check uses the breadcrumb (`nav[aria-label="会话层级"]`
|
|
1381
|
+
* contains a `button[disabled]` with the session title) — NOT the
|
|
1382
|
+
* sidebar's `aria-selected` row. The breadcrumb survives sidebar rebuilds
|
|
1383
|
+
* AND survives the workspace being collapsed (the selected treeitem
|
|
1384
|
+
* itself is removed from the DOM in that case).
|
|
1385
|
+
*/
|
|
1386
|
+
function syncButtonEnabledState(btn) {
|
|
1387
|
+
const hasSession = readCurrentSessionTitle() !== null;
|
|
1388
|
+
const { label, disabled } = buttonCopy();
|
|
1389
|
+
btn.disabled = !hasSession;
|
|
1390
|
+
if (hasSession) {
|
|
1391
|
+
btn.removeAttribute('data-cst-locate-disabled');
|
|
1392
|
+
btn.setAttribute('aria-label', label);
|
|
1393
|
+
// Enabled: the custom hover bubble carries the hint; a title would
|
|
1394
|
+
// summon the browser tooltip on top of it.
|
|
1395
|
+
btn.removeAttribute('title');
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
btn.dataset.cstLocateDisabled = '';
|
|
1399
|
+
btn.setAttribute('aria-label', disabled);
|
|
1400
|
+
// Disabled buttons fire no mouse events, so the custom bubble can't
|
|
1401
|
+
// trigger — fall back to the browser-native title tooltip.
|
|
1402
|
+
btn.setAttribute('title', disabled);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Mount the injected button into the search slot, immediately to the left
|
|
1407
|
+
* of the native search container, or refresh the enabled state of an
|
|
1408
|
+
* already-mounted one. Returns the button element, or null if the search
|
|
1409
|
+
* button is absent (caller should retry on the next DOM mutation).
|
|
1410
|
+
*
|
|
1411
|
+
* Anchors resolve from the stable `button[aria-label="搜索会话"]` hook
|
|
1412
|
+
* (CSS-Modules hashes like `bhn1Oq_*` rotate per DSH build), then walk
|
|
1413
|
+
* up: search container → search slot. The button is inserted into the
|
|
1414
|
+
* slot as a left sibling of the container — NOT inside the container,
|
|
1415
|
+
* which is a 28px round cell with `overflow: hidden` that would both
|
|
1416
|
+
* clip the button and break the header's even 4px icon spacing. CSS
|
|
1417
|
+
* turns the slot into a right-aligned flex row with `gap: 4px`, so the
|
|
1418
|
+
* four header icons (locate / search / view options / add workspace)
|
|
1419
|
+
* share the native spacing.
|
|
1420
|
+
*
|
|
1421
|
+
* `refresh` is the cheap path called from the observer: re-sync the existing
|
|
1422
|
+
* button's enabled state on every tick. The microtask coalesces a burst
|
|
1423
|
+
* of mutations into one re-sync.
|
|
1424
|
+
*/
|
|
1425
|
+
function mountButton(refresh = false) {
|
|
1426
|
+
// Anchor: the localized aria-label first (evaluated per call, so a live
|
|
1427
|
+
// locale switch re-resolves on the next observer tick); structural
|
|
1428
|
+
// fallback when the locale service is unavailable.
|
|
1429
|
+
const label = searchButtonLabel();
|
|
1430
|
+
const searchBtn = document.querySelector(label !== undefined
|
|
1431
|
+
? `button[aria-label="${label}"]`
|
|
1432
|
+
: SEARCH_BUTTON_FALLBACK_SELECTOR);
|
|
1433
|
+
if (searchBtn === null)
|
|
1434
|
+
return null;
|
|
1435
|
+
const searchContainer = searchBtn.parentElement;
|
|
1436
|
+
const slot = searchContainer?.parentElement;
|
|
1437
|
+
if (searchContainer == null || slot == null)
|
|
1438
|
+
return null;
|
|
1439
|
+
// Idempotency: refresh an existing button in place instead of re-mounting.
|
|
1440
|
+
const existing = slot.querySelector('button.cst-locate-btn');
|
|
1441
|
+
if (existing !== null) {
|
|
1442
|
+
if (refresh)
|
|
1443
|
+
syncButtonEnabledState(existing);
|
|
1444
|
+
return existing;
|
|
1445
|
+
}
|
|
1446
|
+
const btn = buildButton();
|
|
1447
|
+
syncButtonEnabledState(btn);
|
|
1448
|
+
slot.insertBefore(btn, searchContainer);
|
|
1449
|
+
return btn;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Setup the live tweak. Returns a disposer that removes the button and
|
|
1453
|
+
* the observer. Safe to call when the search button never appears — the
|
|
1454
|
+
* tweak then stays inert until the next observer tick finds one.
|
|
1455
|
+
*/
|
|
1456
|
+
function setupLocateCurrentSession(ctx) {
|
|
1457
|
+
// Own stylesheet lives and dies with the tweak itself, so index.tsx can
|
|
1458
|
+
// register this setup like any plain injector.
|
|
1459
|
+
const removeStyles = injectLocateCurrentSessionStyles();
|
|
1460
|
+
// Bind the locale namespaces the anchors need. `bind` returns a t that
|
|
1461
|
+
// reads the active locale on every call, so language switches propagate
|
|
1462
|
+
// with no rebind — the next observer tick re-resolves the aria-label
|
|
1463
|
+
// anchors through the same functions. Failures fall back to structural
|
|
1464
|
+
// anchors, exactly like the store lookups below.
|
|
1465
|
+
try {
|
|
1466
|
+
const locale = ctx.locale;
|
|
1467
|
+
if (locale !== undefined && typeof locale.bind === 'function') {
|
|
1468
|
+
tWorkspace = locale.bind('workspace');
|
|
1469
|
+
tConversation = locale.bind('conversation');
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
tWorkspace = undefined;
|
|
1474
|
+
tConversation = undefined;
|
|
1475
|
+
}
|
|
1476
|
+
// Pull the app's own stores the same way project-running-indicator does,
|
|
1477
|
+
// so we can locate the current session's workspace even when its sidebar
|
|
1478
|
+
// group is collapsed (and therefore the session treeitem itself is not
|
|
1479
|
+
// in the DOM). The two store getters are typed via the project's own
|
|
1480
|
+
// `import type` so a host build without them still compiles.
|
|
1481
|
+
let sessionList;
|
|
1482
|
+
let workspaceList;
|
|
1483
|
+
try {
|
|
1484
|
+
sessionList = ctx.get('sessions')?.list;
|
|
1485
|
+
workspaceList = ctx.get('workspaces')?.list;
|
|
1486
|
+
if (sessionList === undefined || workspaceList === undefined
|
|
1487
|
+
|| typeof sessionList.getSnapshot !== 'function'
|
|
1488
|
+
|| typeof workspaceList.getSnapshot !== 'function') {
|
|
1489
|
+
throw new Error('unexpected store shape');
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
sessionList = undefined;
|
|
1494
|
+
workspaceList = undefined;
|
|
1495
|
+
}
|
|
1496
|
+
// Wire the click handler on every button we mount (including re-mounts
|
|
1497
|
+
// after a sidebar rebuild). The handler reads the live store snapshots
|
|
1498
|
+
// at click time, so session/workspace changes propagate without rebind.
|
|
1499
|
+
const onClick = (event) => {
|
|
1500
|
+
event.preventDefault();
|
|
1501
|
+
event.stopPropagation();
|
|
1502
|
+
performLocate(sessionList, workspaceList);
|
|
1503
|
+
};
|
|
1504
|
+
// Try once synchronously so the first paint already shows the button.
|
|
1505
|
+
const initial = mountButton();
|
|
1506
|
+
if (initial !== null)
|
|
1507
|
+
initial.addEventListener('click', onClick);
|
|
1508
|
+
// Watch for the search button to appear / be replaced (DSH rebuilds the
|
|
1509
|
+
// sidebar header on width toggle, workspace switch, and other view
|
|
1510
|
+
// transitions). MutationObserver coalesces a burst of changes into one
|
|
1511
|
+
// microtask. We also listen for attribute changes so the enabled state
|
|
1512
|
+
// tracks the current `aria-selected` row as the user switches sessions
|
|
1513
|
+
// without DSH rebuilding the tree.
|
|
1514
|
+
let scheduled = false;
|
|
1515
|
+
const schedule = () => {
|
|
1516
|
+
if (scheduled)
|
|
1517
|
+
return;
|
|
1518
|
+
scheduled = true;
|
|
1519
|
+
queueMicrotask(() => {
|
|
1520
|
+
scheduled = false;
|
|
1521
|
+
// refresh=true re-syncs the existing button's enabled state on every
|
|
1522
|
+
// tick; mountButton falls back to a no-op mount when the button is
|
|
1523
|
+
// already there. The microtask coalesces the burst into one re-sync.
|
|
1524
|
+
const btn = mountButton(true);
|
|
1525
|
+
if (btn !== null && !btn.dataset.cstLocateWired) {
|
|
1526
|
+
btn.addEventListener('click', onClick);
|
|
1527
|
+
btn.dataset.cstLocateWired = '1';
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
};
|
|
1531
|
+
const observer = new MutationObserver(schedule);
|
|
1532
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-selected', 'aria-expanded'] });
|
|
1533
|
+
return () => {
|
|
1534
|
+
observer.disconnect();
|
|
1535
|
+
hideTooltip();
|
|
1536
|
+
removeStyles();
|
|
1537
|
+
for (const btn of document.querySelectorAll('button.cst-locate-btn')) {
|
|
1538
|
+
btn.removeEventListener('click', onClick);
|
|
1539
|
+
btn.remove();
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
__modules["./tweaks/project-running-indicator.js"] = function(module, exports, require, __load_) {
|
|
1545
|
+
"use strict";
|
|
1546
|
+
/**
|
|
1547
|
+
* dsh-style-tweaks — project-running-indicator tweak.
|
|
1548
|
+
*
|
|
1549
|
+
* The first non-pure-CSS tweak: it renders the *app's own* `StateDot`
|
|
1550
|
+
* (the animated pixel-chase shown left of a running conversation's title,
|
|
1551
|
+
* `ui-primitives/src/StateDot.tsx`) on the right side of every project
|
|
1552
|
+
* directory header row in the sidebar, so a running conversation stays
|
|
1553
|
+
* visible even when its group is collapsed.
|
|
1554
|
+
*
|
|
1555
|
+
* ## Why DOM patching
|
|
1556
|
+
*
|
|
1557
|
+
* DSH's project header (`ProjectRowItem`, `ui-workspace/src/client/rows/
|
|
1558
|
+
* Rows.tsx:112`) exposes no plugin slot, and the `sidebar.workspaces` seat
|
|
1559
|
+
* is already occupied by the stock browser — so the header row can only be
|
|
1560
|
+
* decorated from outside, via a MutationObserver. Consistent with this
|
|
1561
|
+
* repo's selector philosophy, the engine anchors exclusively on hand-written
|
|
1562
|
+
* DOM facts that survive CSS-Modules hashing:
|
|
1563
|
+
*
|
|
1564
|
+
* - group header rows are the only `role="treeitem"` elements carrying
|
|
1565
|
+
* `aria-expanded` (session rows and flat-list rows never have it);
|
|
1566
|
+
* - the row's title text is the only `:scope > span > span` descendant
|
|
1567
|
+
* with non-empty text (folder/chevron slots hold bare SVGs, the action
|
|
1568
|
+
* buttons' labels live on `aria-label`, not text).
|
|
1569
|
+
*
|
|
1570
|
+
* ## Data source
|
|
1571
|
+
*
|
|
1572
|
+
* Running state is read from the app's own stores, not inferred from the
|
|
1573
|
+
* DOM (a collapsed group hides its child rows, so the DOM cannot answer):
|
|
1574
|
+
*
|
|
1575
|
+
* - `ctx.get('sessions').list` — snapshot store of `SessionSummary`s
|
|
1576
|
+
* (`byId[id].running`);
|
|
1577
|
+
* - `ctx.get('workspaces').list` — `WorkspaceSnapshot.items`, each
|
|
1578
|
+
* `WorkspaceView.sessionIds` mapping sessions to directories.
|
|
1579
|
+
*
|
|
1580
|
+
* Membership is computed from the workspace arrays (mirroring
|
|
1581
|
+
* `deriveGroups` in `ui-workspace/src/client/tree.ts`): archived and blank
|
|
1582
|
+
* sessions are skipped, sessions not in any workspace count toward the
|
|
1583
|
+
* "Ungrouped" bucket. A running subagent marks its top-level ancestor
|
|
1584
|
+
* session busy (same lineage walk as `indexSubagentDescendants`), which is
|
|
1585
|
+
* what the title's own dot shows.
|
|
1586
|
+
*
|
|
1587
|
+
* Header rows are matched to workspaces by title text; a header whose label
|
|
1588
|
+
* matches no workspace title is the Ungrouped bucket. This avoids depending
|
|
1589
|
+
* on DSH's locale copy for the bucket label (and tolerates renames for
|
|
1590
|
+
* free, since the title text *is* the match key).
|
|
1591
|
+
*
|
|
1592
|
+
* ## Rendering
|
|
1593
|
+
*
|
|
1594
|
+
* The dot is the real `StateDot` imported from
|
|
1595
|
+
* `@deepseek-ai/dsh-client-ui-primitives`. That package is in the client
|
|
1596
|
+
* shell's `PLATFORM_MODULES` baseline, so at runtime the import resolves to
|
|
1597
|
+
* the app's shared instance — its CSS module (and the global
|
|
1598
|
+
* `dsh-state-dot-chase` keyframes) and theme tokens are already loaded, and
|
|
1599
|
+
* the animation is pixel-identical to the one on session titles. The dot is
|
|
1600
|
+
* mounted with a `react-dom/client` root per decorated row and unmounted
|
|
1601
|
+
* whenever the group goes idle, the row leaves the DOM, or the tweak is
|
|
1602
|
+
* disabled.
|
|
1603
|
+
*/
|
|
1604
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1605
|
+
exports.setupProjectRunningIndicator = setupProjectRunningIndicator;
|
|
1606
|
+
const react_1 = require("react");
|
|
1607
|
+
const client_1 = require("react-dom/client");
|
|
1608
|
+
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1609
|
+
const INDICATOR_CSS = `
|
|
1610
|
+
span[data-cst-proj-indicator] {
|
|
1611
|
+
display: inline-flex;
|
|
1612
|
+
align-items: center;
|
|
1613
|
+
flex: none;
|
|
1614
|
+
margin-left: 2px;
|
|
1615
|
+
pointer-events: none;
|
|
1616
|
+
}
|
|
1617
|
+
`;
|
|
1618
|
+
const INDICATOR_CSS_ID = 'cst-project-running-indicator';
|
|
1619
|
+
/** Group header rows are the only treeitems with aria-expanded (Rows.tsx:143). */
|
|
1620
|
+
const HEADER_SELECTOR = '[role="treeitem"][aria-expanded]';
|
|
1621
|
+
function installIndicatorStyles() {
|
|
1622
|
+
let style = document.querySelector(`style[data-tweak-css="${INDICATOR_CSS_ID}"]`);
|
|
1623
|
+
if (style === null) {
|
|
1624
|
+
style = document.createElement('style');
|
|
1625
|
+
style.dataset.tweak = 'cst';
|
|
1626
|
+
style.dataset.tweakCss = INDICATOR_CSS_ID;
|
|
1627
|
+
style.textContent = INDICATOR_CSS;
|
|
1628
|
+
document.head.appendChild(style);
|
|
1629
|
+
}
|
|
1630
|
+
return () => { style?.remove(); };
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Derive which group labels must show a dot. Mirrors `deriveGroups`
|
|
1634
|
+
* membership rules: archived and blank sessions never render as rows, and
|
|
1635
|
+
* subagent rows belong to their top-level ancestor's group.
|
|
1636
|
+
*/
|
|
1637
|
+
function computeGroupBusy(sessions, workspaces) {
|
|
1638
|
+
const list = sessions.getSnapshot();
|
|
1639
|
+
const snapshot = workspaces.getSnapshot();
|
|
1640
|
+
const archived = new Set(snapshot.archivedSessionIds);
|
|
1641
|
+
// Running subagents light up their top-level ancestor (Rows.tsx
|
|
1642
|
+
// sessionStatuses shows 'ongoing' for runningSubagentCount > 0).
|
|
1643
|
+
const busyAncestors = new Set();
|
|
1644
|
+
for (const summary of Object.values(list.byId)) {
|
|
1645
|
+
if (summary === undefined)
|
|
1646
|
+
continue;
|
|
1647
|
+
if (!summary.running || summary.origin !== 'subagent')
|
|
1648
|
+
continue;
|
|
1649
|
+
const seen = new Set();
|
|
1650
|
+
let current = summary;
|
|
1651
|
+
while (current?.origin === 'subagent' && current.parentId !== undefined && !seen.has(current.id)) {
|
|
1652
|
+
seen.add(current.id);
|
|
1653
|
+
current = list.byId[current.parentId];
|
|
1654
|
+
}
|
|
1655
|
+
if (current !== undefined && current.origin !== 'subagent')
|
|
1656
|
+
busyAncestors.add(current.id);
|
|
1657
|
+
}
|
|
1658
|
+
const isBusy = (id) => {
|
|
1659
|
+
const summary = list.byId[id];
|
|
1660
|
+
if (summary === undefined)
|
|
1661
|
+
return false;
|
|
1662
|
+
return summary.running || busyAncestors.has(id);
|
|
1663
|
+
};
|
|
1664
|
+
const busyTitles = new Set();
|
|
1665
|
+
const knownTitles = new Set();
|
|
1666
|
+
const grouped = new Set();
|
|
1667
|
+
for (const workspace of snapshot.items) {
|
|
1668
|
+
knownTitles.add(workspace.title);
|
|
1669
|
+
for (const id of workspace.sessionIds) {
|
|
1670
|
+
grouped.add(id);
|
|
1671
|
+
if (archived.has(id))
|
|
1672
|
+
continue;
|
|
1673
|
+
const summary = list.byId[id];
|
|
1674
|
+
// Blank sessions are hidden from the browser (tree.ts deriveGroups).
|
|
1675
|
+
if (summary === undefined || summary.blank)
|
|
1676
|
+
continue;
|
|
1677
|
+
if (isBusy(id))
|
|
1678
|
+
busyTitles.add(workspace.title);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// Ungrouped bucket: host-list rows in no workspace (ids excludes the
|
|
1682
|
+
// breadcrumb-only subagent routes that byId additionally carries).
|
|
1683
|
+
let ungroupedBusy = false;
|
|
1684
|
+
for (const id of list.ids) {
|
|
1685
|
+
if (grouped.has(id) || archived.has(id))
|
|
1686
|
+
continue;
|
|
1687
|
+
const summary = list.byId[id];
|
|
1688
|
+
if (summary === undefined || summary.blank || summary.origin === 'subagent')
|
|
1689
|
+
continue;
|
|
1690
|
+
if (isBusy(id)) {
|
|
1691
|
+
ungroupedBusy = true;
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return { busyTitles, knownTitles, ungroupedBusy };
|
|
1696
|
+
}
|
|
1697
|
+
/** Title text of a header row: the only span-in-span with visible text. */
|
|
1698
|
+
function rowLabel(row) {
|
|
1699
|
+
for (const span of row.querySelectorAll(':scope > span > span')) {
|
|
1700
|
+
const label = span.textContent?.trim();
|
|
1701
|
+
if (label !== undefined && label.length > 0)
|
|
1702
|
+
return label;
|
|
1703
|
+
}
|
|
1704
|
+
return undefined;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Mount/unmount the live tweak. Returns the disposer; safe to call when the
|
|
1708
|
+
* app stores are absent (unknown host build) — the tweak then stays inert.
|
|
1709
|
+
*
|
|
1710
|
+
* HMR-safe: the guard lives on `window` so it survives module reloads. A
|
|
1711
|
+
* hot-reload that calls this again before the previous cleanup runs will
|
|
1712
|
+
* tear down the stale instance first instead of stacking two
|
|
1713
|
+
* MutationObservers / two dots per row.
|
|
1714
|
+
*/
|
|
1715
|
+
const GLOBAL_KEY = '__cst_project_running_indicator_cleanup__';
|
|
1716
|
+
function getGlobalCleanup() {
|
|
1717
|
+
return window[GLOBAL_KEY];
|
|
1718
|
+
}
|
|
1719
|
+
function setGlobalCleanup(fn) {
|
|
1720
|
+
;
|
|
1721
|
+
window[GLOBAL_KEY] = fn;
|
|
1722
|
+
}
|
|
1723
|
+
function setupProjectRunningIndicator(ctx) {
|
|
1724
|
+
const previous = getGlobalCleanup();
|
|
1725
|
+
if (typeof previous === 'function') {
|
|
1726
|
+
previous();
|
|
1727
|
+
setGlobalCleanup(undefined);
|
|
1728
|
+
}
|
|
1729
|
+
const removeStyles = installIndicatorStyles();
|
|
1730
|
+
let sessionList;
|
|
1731
|
+
let workspaceList;
|
|
1732
|
+
try {
|
|
1733
|
+
// The services expose their state as snapshot stores: `sessions.list`
|
|
1734
|
+
// (SessionListState) and `workspaces.list` (WorkspaceSnapshot).
|
|
1735
|
+
sessionList = ctx.get('sessions').list;
|
|
1736
|
+
workspaceList = ctx.get('workspaces').list;
|
|
1737
|
+
if (typeof sessionList?.subscribe !== 'function' || typeof workspaceList?.subscribe !== 'function') {
|
|
1738
|
+
throw new Error('unexpected store shape');
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
catch {
|
|
1742
|
+
// Unknown host build without the expected services: keep the CSS inert.
|
|
1743
|
+
return () => { removeStyles(); };
|
|
1744
|
+
}
|
|
1745
|
+
const indicators = new Map();
|
|
1746
|
+
const removeIndicator = (row) => {
|
|
1747
|
+
const indicator = indicators.get(row);
|
|
1748
|
+
if (indicator === undefined)
|
|
1749
|
+
return;
|
|
1750
|
+
indicators.delete(row);
|
|
1751
|
+
indicator.root.unmount();
|
|
1752
|
+
indicator.host.remove();
|
|
1753
|
+
// After this point the entry is gone from `indicators` and the host is
|
|
1754
|
+
// detached, so the React root and DOM node are both GC-eligible. Do not
|
|
1755
|
+
// call `indicator.root.render(...)` past here — it's a defunct handle.
|
|
1756
|
+
};
|
|
1757
|
+
const syncRow = (row, busy) => {
|
|
1758
|
+
const label = rowLabel(row);
|
|
1759
|
+
if (label === undefined)
|
|
1760
|
+
return;
|
|
1761
|
+
// The Ungrouped bucket is the header whose label matches no workspace
|
|
1762
|
+
// title — its label is DSH locale copy, so it is matched negatively.
|
|
1763
|
+
const active = busy.busyTitles.has(label)
|
|
1764
|
+
|| (busy.ungroupedBusy && !busy.knownTitles.has(label));
|
|
1765
|
+
let indicator = indicators.get(row);
|
|
1766
|
+
if (!active) {
|
|
1767
|
+
// Group went idle (or the row lost its label): unmount the dot.
|
|
1768
|
+
if (indicator !== undefined)
|
|
1769
|
+
removeIndicator(row);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (indicator === undefined) {
|
|
1773
|
+
const host = document.createElement('span');
|
|
1774
|
+
host.dataset.cstProjIndicator = '';
|
|
1775
|
+
row.insertBefore(host, row.lastElementChild);
|
|
1776
|
+
indicator = { host, root: (0, client_1.createRoot)(host), shown: false };
|
|
1777
|
+
indicators.set(row, indicator);
|
|
1778
|
+
}
|
|
1779
|
+
else if (!indicator.host.isConnected) {
|
|
1780
|
+
// Don't re-render on reattach — `shown` stays true and the root keeps
|
|
1781
|
+
// its existing vDOM, so the StateDot's animation continues uninterrupted
|
|
1782
|
+
// when the row leaves and re-enters the DOM.
|
|
1783
|
+
row.insertBefore(indicator.host, row.lastElementChild);
|
|
1784
|
+
}
|
|
1785
|
+
if (!indicator.shown) {
|
|
1786
|
+
indicator.shown = true;
|
|
1787
|
+
indicator.root.render((0, react_1.createElement)(dsh_client_ui_primitives_1.StateDot, { state: 'ongoing' }));
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
const sync = () => {
|
|
1791
|
+
// Rows removed from the document: their entries must go (Map, not
|
|
1792
|
+
// WeakMap, because we need isConnected checks + explicit removal).
|
|
1793
|
+
for (const row of [...indicators.keys()]) {
|
|
1794
|
+
if (!row.isConnected)
|
|
1795
|
+
removeIndicator(row);
|
|
1796
|
+
}
|
|
1797
|
+
const busy = computeGroupBusy(sessionList, workspaceList);
|
|
1798
|
+
for (const row of document.querySelectorAll(HEADER_SELECTOR)) {
|
|
1799
|
+
syncRow(row, busy);
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
// Coalesce a burst of DOM mutations into one scan per microtask: the
|
|
1803
|
+
// observer spans `document.body` and every streaming chunk lights it up
|
|
1804
|
+
// for a few ticks, but they all settle before the microtask queue drains.
|
|
1805
|
+
let syncScheduled = false;
|
|
1806
|
+
const scheduleSync = () => {
|
|
1807
|
+
if (syncScheduled)
|
|
1808
|
+
return;
|
|
1809
|
+
syncScheduled = true;
|
|
1810
|
+
queueMicrotask(() => {
|
|
1811
|
+
syncScheduled = false;
|
|
1812
|
+
sync();
|
|
1813
|
+
});
|
|
1814
|
+
};
|
|
1815
|
+
const observer = new MutationObserver(() => { scheduleSync(); });
|
|
1816
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1817
|
+
const unsubscribeSessions = sessionList.subscribe(() => { scheduleSync(); });
|
|
1818
|
+
const unsubscribeWorkspaces = workspaceList.subscribe(() => { scheduleSync(); });
|
|
1819
|
+
sync();
|
|
1820
|
+
const cleanup = () => {
|
|
1821
|
+
unsubscribeSessions();
|
|
1822
|
+
unsubscribeWorkspaces();
|
|
1823
|
+
observer.disconnect();
|
|
1824
|
+
for (const row of [...indicators.keys()])
|
|
1825
|
+
removeIndicator(row);
|
|
1826
|
+
removeStyles();
|
|
1827
|
+
setGlobalCleanup(undefined);
|
|
1828
|
+
};
|
|
1829
|
+
setGlobalCleanup(cleanup);
|
|
1830
|
+
return cleanup;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
__modules["./tweaks/registry.js"] = function(module, exports, require, __load_) {
|
|
1834
|
+
"use strict";
|
|
1835
|
+
/**
|
|
1836
|
+
* dsh-style-tweaks — tweak registry.
|
|
1837
|
+
*
|
|
1838
|
+
* Central catalogue of all CSS tweaks the plugin can inject. Each tweak is
|
|
1839
|
+
* described once here; the client iterates this list to (a) render Settings
|
|
1840
|
+
* panel toggles and (b) mount / unmount the corresponding styles live.
|
|
1841
|
+
*/
|
|
1842
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1843
|
+
exports.TWEAKS = void 0;
|
|
1844
|
+
/** All tweaks, in display order. Add new entries here. */
|
|
1845
|
+
exports.TWEAKS = [
|
|
1846
|
+
{
|
|
1847
|
+
id: 'stable-table',
|
|
1848
|
+
settingKey: 'stableTable',
|
|
1849
|
+
defaultEnabled: true,
|
|
1850
|
+
titleKey: 'tweak.stableTable.title',
|
|
1851
|
+
descriptionKey: 'tweak.stableTable.description',
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
id: 'stable-turn-rail',
|
|
1855
|
+
settingKey: 'stableTurnRail',
|
|
1856
|
+
defaultEnabled: true,
|
|
1857
|
+
titleKey: 'tweak.stableTurnRail.title',
|
|
1858
|
+
descriptionKey: 'tweak.stableTurnRail.description',
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
id: 'code-block-flush-top',
|
|
1862
|
+
settingKey: 'codeBlockFlushTop',
|
|
1863
|
+
defaultEnabled: true,
|
|
1864
|
+
titleKey: 'tweak.codeBlockFlushTop.title',
|
|
1865
|
+
descriptionKey: 'tweak.codeBlockFlushTop.description',
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
id: 'project-running-indicator',
|
|
1869
|
+
settingKey: 'projectRunningIndicator',
|
|
1870
|
+
defaultEnabled: true,
|
|
1871
|
+
titleKey: 'tweak.projectRunningIndicator.title',
|
|
1872
|
+
descriptionKey: 'tweak.projectRunningIndicator.description',
|
|
1873
|
+
},
|
|
1874
|
+
{
|
|
1875
|
+
id: 'locate-current-session',
|
|
1876
|
+
settingKey: 'locateCurrentSession',
|
|
1877
|
+
defaultEnabled: true,
|
|
1878
|
+
titleKey: 'tweak.locateCurrentSession.title',
|
|
1879
|
+
descriptionKey: 'tweak.locateCurrentSession.description',
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
id: 'settings-nav-scroll',
|
|
1883
|
+
settingKey: 'settingsNavScroll',
|
|
1884
|
+
defaultEnabled: true,
|
|
1885
|
+
titleKey: 'tweak.settingsNavScroll.title',
|
|
1886
|
+
descriptionKey: 'tweak.settingsNavScroll.description',
|
|
1887
|
+
},
|
|
1888
|
+
];
|
|
1889
|
+
};
|
|
1890
|
+
__modules["./tweaks/settings-nav-scroll.js"] = function(module, exports, require, __load_) {
|
|
1891
|
+
"use strict";
|
|
1892
|
+
/**
|
|
1893
|
+
* dsh-style-tweaks — settings-nav-scroll tweak.
|
|
1894
|
+
*
|
|
1895
|
+
* Makes the settings dialog's LEFT nav column scrollable once its section
|
|
1896
|
+
* list outgrows the fixed-height panel, instead of silently clipping the
|
|
1897
|
+
* entries at the bottom. Without this, a user with several plugin-registered
|
|
1898
|
+
* sections cannot reach the last items at all — the panel hides them and
|
|
1899
|
+
* nothing scrolls. The scrollbar itself is an overlay-style thin strip:
|
|
1900
|
+
* parked in the rail's spare padding so it never squeezes the menu, and
|
|
1901
|
+
* visible only while the list is actually being scrolled (macOS-style),
|
|
1902
|
+
* driven by a scroll listener rather than `:hover`.
|
|
1903
|
+
*
|
|
1904
|
+
* ## Symptom
|
|
1905
|
+
*
|
|
1906
|
+
* The settings dialog is DSH's `SettingsRoot`
|
|
1907
|
+
* (`packages/client/ui-settings-general/src/client/SettingsRoot.tsx`):
|
|
1908
|
+
*
|
|
1909
|
+
* <div className={css.panel} role="dialog" aria-modal="true"> ← 800px, overflow: hidden
|
|
1910
|
+
* <nav className={css.nav}> ← 188px rail
|
|
1911
|
+
* <div className={css.navTitle}>…</div>
|
|
1912
|
+
* <div className={css.navList}> ← the section buttons
|
|
1913
|
+
* <button className={css.navCell}>…</button> × N
|
|
1914
|
+
* </div>
|
|
1915
|
+
* </nav>
|
|
1916
|
+
* <div className={css.content}> … </div>
|
|
1917
|
+
* </div>
|
|
1918
|
+
*
|
|
1919
|
+
* The panel has ONE height for every section (`SettingsRoot.module.css`:
|
|
1920
|
+
* `height: min(800px, calc(100vh - 48px)); overflow: hidden`) so it does not
|
|
1921
|
+
* resize under the pointer between nav clicks, and its own comment states
|
|
1922
|
+
* "whatever does not fit scrolls in `.options`" — the right content column
|
|
1923
|
+
* got `overflow-y: auto`, but the LEFT nav never received the same
|
|
1924
|
+
* treatment. `.navList` is a plain flex column (`gap: 4px`, default
|
|
1925
|
+
* `flex: 0 1 auto`, no `min-height: 0`, no overflow), and its `min-height:
|
|
1926
|
+
* auto` forbids shrinking below the content, so the list simply overflows
|
|
1927
|
+
* `.nav` and the `.panel`'s `overflow: hidden` clips whatever spills past
|
|
1928
|
+
* the bottom edge. With enough plugin sections (this plugin alone adds one),
|
|
1929
|
+
* the last nav entries become unreachable.
|
|
1930
|
+
*
|
|
1931
|
+
* ## Fix — part 1 (CSS): make the list the scroll container
|
|
1932
|
+
*
|
|
1933
|
+
* One rule turning the nav list into the scroll container of the nav rail's
|
|
1934
|
+
* leftover height:
|
|
1935
|
+
*
|
|
1936
|
+
* flex: 1 — the list owns the column height below the title
|
|
1937
|
+
* (previously implicit; nav is a flex column whose
|
|
1938
|
+
* only other child is the title row).
|
|
1939
|
+
* min-height: 0 — the load-bearing line: overrides the flex item's
|
|
1940
|
+
* `min-height: auto`, allowing the list to be shorter
|
|
1941
|
+
* than its content so overflow becomes possible.
|
|
1942
|
+
* overflow-y: auto — scroll only when entries exceed the height; a short
|
|
1943
|
+
* list looks exactly like stock DSH.
|
|
1944
|
+
* overscroll-behavior: contain — at the list's scroll end the chain stops
|
|
1945
|
+
* here instead of scrolling some page behind the modal.
|
|
1946
|
+
* padding-bottom: 12px — breathing room after the last entry when fully
|
|
1947
|
+
* scrolled (`.nav` has no bottom padding by design);
|
|
1948
|
+
* invisible when the list does not scroll.
|
|
1949
|
+
* margin-inline-end: -12px — parks the bar OUT of the content column,
|
|
1950
|
+
* 8px clear of the cells (see part 2).
|
|
1951
|
+
*
|
|
1952
|
+
* The title row stays fixed and the panel keeps its size — no layout shift
|
|
1953
|
+
* anywhere else.
|
|
1954
|
+
*
|
|
1955
|
+
* ## Fix — part 2 (CSS): the bar parks in the rail's padding gutter, clear of the cells
|
|
1956
|
+
*
|
|
1957
|
+
* A classic scrollbar consumes its width from the element's content box —
|
|
1958
|
+
* the first shipped version narrowed the cells 164 → 156px the moment the
|
|
1959
|
+
* bar appeared, and the bar sat there permanently while the list overflowed.
|
|
1960
|
+
* Native Chromium scrollbars have no CSS "overlay" mode, but the classic
|
|
1961
|
+
* bar can be parked out of the content column:
|
|
1962
|
+
*
|
|
1963
|
+
* margin-inline-end: -12px — the list's border box stretches the rail's
|
|
1964
|
+
* full 12px right padding into itself: 8px gap + 4px
|
|
1965
|
+
* bar strip. The thumb therefore sits 8px clear of the
|
|
1966
|
+
* entries (values user-tuned in the console as W=4 /
|
|
1967
|
+
* GAP=8, then solidified into the tweak) and floats
|
|
1968
|
+
* over the rail's padding area like an overlay bar,
|
|
1969
|
+
* flush with the gutter edge before the content column
|
|
1970
|
+
* boundary.
|
|
1971
|
+
*
|
|
1972
|
+
* [class*="_navCell"] { max-width: 164px } — the park widens the list's
|
|
1973
|
+
* content box whenever the bar is ABSENT (no overflow →
|
|
1974
|
+
* content 176px), which made the hover highlight paint
|
|
1975
|
+
* 12px wider than stock in that state. Capping the cells
|
|
1976
|
+
* at DSH's stock width (188 nav − 2×12 padding) pins
|
|
1977
|
+
* them to 164px in EVERY state — bar present, bar
|
|
1978
|
+
* absent, hovering, scrolling — so the rail's geometry
|
|
1979
|
+
* never differs from stock by a pixel. If DSH ever
|
|
1980
|
+
* widens the rail, the cap only over-narrows cosmetically;
|
|
1981
|
+
* if it narrows, stretch wins below the cap and nothing
|
|
1982
|
+
* changes.
|
|
1983
|
+
*
|
|
1984
|
+
* ::-webkit-scrollbar { width: 4px } — thinner than DSH's global 8px
|
|
1985
|
+
* skin; colour/radius of the visible thumb come from
|
|
1986
|
+
* DSH's global thumb rules (l2 tokens inherited from
|
|
1987
|
+
* the panel; the 4px radius auto-clamps to 2px on this
|
|
1988
|
+
* width), so only geometry lives here.
|
|
1989
|
+
*
|
|
1990
|
+
* ## Fix — part 3 (JS): show only while scrolling
|
|
1991
|
+
*
|
|
1992
|
+
* The idle bar must be invisible, and the first attempt used the classic
|
|
1993
|
+
* hover-reveal (`:not(:hover)::-webkit-scrollbar-thumb → transparent`).
|
|
1994
|
+
* Chromium does not reliably repaint custom scrollbar pseudo-elements when
|
|
1995
|
+
* only the HOST's `:hover` state changes — the thumb kept its last painted
|
|
1996
|
+
* style until an unrelated invalidation; in practice the bar appeared when
|
|
1997
|
+
* clicking a nav cell (React re-rendered the list → repaint) instead of
|
|
1998
|
+
* while scrolling, which is exactly backwards.
|
|
1999
|
+
*
|
|
2000
|
+
* So the show condition is driven by real activity: `scroll` events on the
|
|
2001
|
+
* list toggle a `cst-nav-scroll-show` class on the list itself, and ~800ms
|
|
2002
|
+
* after the last scroll event it is removed (macOS overlay-bar timing).
|
|
2003
|
+
* Class changes restyle the element itself, so the scrollbar repaints
|
|
2004
|
+
* reliably — no dependence on host pseudo-state. Wheel-scrolling requires
|
|
2005
|
+
* the pointer on the list, thumb drags fire scroll, and programmatic
|
|
2006
|
+
* scrolls (keyboard focus moving through the cells) count as activity too;
|
|
2007
|
+
* idle hovering shows nothing, matching "hide when not scrolling".
|
|
2008
|
+
*
|
|
2009
|
+
* The JS surface is deliberately small: a MutationObserver keeps the
|
|
2010
|
+
* scroll listener attached to whichever element currently matches the
|
|
2011
|
+
* selector (the dialog unmounts when closed and remounts when opened),
|
|
2012
|
+
* one passive `scroll` listener per attached list, one idle timer.
|
|
2013
|
+
*
|
|
2014
|
+
* ## Selector strategy
|
|
2015
|
+
*
|
|
2016
|
+
* DSH hashes CSS Modules class names with the `[hash]_[local]` pattern
|
|
2017
|
+
* (`packages/client/tsdown.client.ts`: `cssModules: { pattern:
|
|
2018
|
+
* '[hash]_[local]' }`), so the compiled class is `<hash>_navList` — the hash
|
|
2019
|
+
* is unpredictable across DSH versions, the local name is not. We match the
|
|
2020
|
+
* fragment with `[class*="_navList"]` and pin it structurally to the one
|
|
2021
|
+
* place that renders it: the modal panel's nav rail
|
|
2022
|
+
* (`div[role="dialog"][aria-modal="true"] > nav > …`). Both anchors are
|
|
2023
|
+
* redundant on their own; together they survive either DSH reshuffling the
|
|
2024
|
+
* dialog structure or renaming the local class.
|
|
2025
|
+
*
|
|
2026
|
+
* `navList` is defined in exactly one CSS module in DSH
|
|
2027
|
+
* (`ui-settings-general/src/client/SettingsRoot.module.css`), so the
|
|
2028
|
+
* fragment cannot collide with other dialogs (Modal primitive, lightbox,
|
|
2029
|
+
* onboarding steps, …) even before the structural pin.
|
|
2030
|
+
*
|
|
2031
|
+
* ## Specificity
|
|
2032
|
+
*
|
|
2033
|
+
* DSH's `.navList { display: flex; flex-direction: column; gap: 4px }` is
|
|
2034
|
+
* (0,0,1,0). The base rule is (0,3,2) — three attribute selectors plus two
|
|
2035
|
+
* element selectors — and DSH sets none of the base properties we touch.
|
|
2036
|
+
* The idle-thumb rule (0,3,2) beats DSH's unscoped global thumb skin
|
|
2037
|
+
* (0,0,1); the show rule adds a class (0,4,2) and its hover variant (0,5,2)
|
|
2038
|
+
* restores DSH's idle/hover token pair while shown. No `!important`
|
|
2039
|
+
* anywhere.
|
|
2040
|
+
*
|
|
2041
|
+
* ## Scrollbar skin
|
|
2042
|
+
*
|
|
2043
|
+
* The visible thumb reads DSH's themed scrollbar tokens: the panel rebinds
|
|
2044
|
+
* `--dsh-scrollbar-thumb{,-hover}` to the l2 elevation tokens (declared on
|
|
2045
|
+
* `.panel` so they inherit into any scrolling descendant); we reference the
|
|
2046
|
+
* same custom properties so the nav bar matches the right column's look,
|
|
2047
|
+
* only thinner.
|
|
2048
|
+
*/
|
|
2049
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2050
|
+
exports.SETTINGS_NAV_SCROLL_CSS_ID = void 0;
|
|
2051
|
+
exports.setupSettingsNavScroll = setupSettingsNavScroll;
|
|
2052
|
+
const NAV_LIST_SELECTOR = 'div[role="dialog"][aria-modal="true"] > nav > [class*="_navList"]';
|
|
2053
|
+
/** Class the scroll driver toggles to reveal the thumb. Lives on the list itself. */
|
|
2054
|
+
const SHOW_CLASS = 'cst-nav-scroll-show';
|
|
2055
|
+
/** Overlay-bar idle delay: fade out this long after the last scroll event. */
|
|
2056
|
+
const HIDE_DELAY_MS = 800;
|
|
2057
|
+
const SETTINGS_NAV_SCROLL_CSS = `
|
|
2058
|
+
/* Settings dialog left nav list: become the scroll container once the
|
|
2059
|
+
* section entries outgrow the fixed-height panel, with the bar parked in
|
|
2060
|
+
* the rail's right padding gutter — 8px gap + 4px strip = the full 12px
|
|
2061
|
+
* padding (see the module doc for the full derivation — panel
|
|
2062
|
+
* overflow:hidden clips the overflow today, and .options only gave the
|
|
2063
|
+
* RIGHT column a scroll treatment). */
|
|
2064
|
+
${NAV_LIST_SELECTOR} {
|
|
2065
|
+
flex: 1;
|
|
2066
|
+
min-height: 0;
|
|
2067
|
+
overflow-y: auto;
|
|
2068
|
+
overscroll-behavior: contain;
|
|
2069
|
+
padding-bottom: 12px;
|
|
2070
|
+
margin-inline-end: -12px;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/* Pin the cells to DSH's stock width (188 nav − 2×12 padding): the parked
|
|
2074
|
+
* margin widens the list's content box whenever the bar is absent, and
|
|
2075
|
+
* without the cap the hover highlight would paint 6px wider than stock in
|
|
2076
|
+
* that state. With the cap the cells stay stock-width in every state. */
|
|
2077
|
+
${NAV_LIST_SELECTOR} > [class*="_navCell"] {
|
|
2078
|
+
max-width: 164px;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/* Thin bar: 4px vs DSH's global 8px skin. Idle thumb is blanked; the
|
|
2082
|
+
* scroll driver below adds ${SHOW_CLASS} to reveal it with DSH's own l2
|
|
2083
|
+
* token colours while the list is actually scrolling. */
|
|
2084
|
+
${NAV_LIST_SELECTOR}::-webkit-scrollbar {
|
|
2085
|
+
width: 4px;
|
|
2086
|
+
}
|
|
2087
|
+
${NAV_LIST_SELECTOR}::-webkit-scrollbar-thumb {
|
|
2088
|
+
background: transparent;
|
|
2089
|
+
transition: background-color .15s ease;
|
|
2090
|
+
}
|
|
2091
|
+
${NAV_LIST_SELECTOR}.${SHOW_CLASS}::-webkit-scrollbar-thumb {
|
|
2092
|
+
background: var(--dsh-scrollbar-thumb);
|
|
2093
|
+
}
|
|
2094
|
+
${NAV_LIST_SELECTOR}.${SHOW_CLASS}::-webkit-scrollbar-thumb:hover {
|
|
2095
|
+
background: var(--dsh-scrollbar-thumb-hover);
|
|
2096
|
+
}
|
|
2097
|
+
`;
|
|
2098
|
+
exports.SETTINGS_NAV_SCROLL_CSS_ID = 'cst-settings-nav-scroll';
|
|
2099
|
+
/** Idempotent style install, same pattern as the pure-CSS tweaks — with one
|
|
2100
|
+
* upgrade: if HMR re-runs setup while the previous style element survived
|
|
2101
|
+
* (teardown order across a hot swap is not guaranteed), its text is from the
|
|
2102
|
+
* OLD bundle. Refresh the text unconditionally so the new rules always win. */
|
|
2103
|
+
function installNavScrollStyles() {
|
|
2104
|
+
let style = document.querySelector(`style[data-tweak-css="${exports.SETTINGS_NAV_SCROLL_CSS_ID}"]`);
|
|
2105
|
+
if (style === null) {
|
|
2106
|
+
style = document.createElement('style');
|
|
2107
|
+
style.dataset.tweak = 'cst';
|
|
2108
|
+
style.dataset.tweakCss = exports.SETTINGS_NAV_SCROLL_CSS_ID;
|
|
2109
|
+
document.head.appendChild(style);
|
|
2110
|
+
}
|
|
2111
|
+
if (style.textContent !== SETTINGS_NAV_SCROLL_CSS)
|
|
2112
|
+
style.textContent = SETTINGS_NAV_SCROLL_CSS;
|
|
2113
|
+
return () => { style?.remove(); };
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* HMR duplicate-setup guard (same pattern as project-running-indicator): a
|
|
2117
|
+
* hot reload can run setup again before the previous effect's cleanup ran;
|
|
2118
|
+
* run the stale cleanup first so two drivers never fight over one list.
|
|
2119
|
+
*/
|
|
2120
|
+
const GLOBAL_KEY = '__cst_settings_nav_scroll_cleanup__';
|
|
2121
|
+
function getGlobalCleanup() {
|
|
2122
|
+
return window[GLOBAL_KEY];
|
|
2123
|
+
}
|
|
2124
|
+
function setGlobalCleanup(fn) {
|
|
2125
|
+
;
|
|
2126
|
+
window[GLOBAL_KEY] = fn;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Mount the scroll driver: keep the scroll listener on whichever element
|
|
2130
|
+
* currently matches the nav-list selector, reveal the thumb while it
|
|
2131
|
+
* scrolls, fade it out after the idle delay.
|
|
2132
|
+
* @param _ctx - client context (unused; signature parity with JS tweaks).
|
|
2133
|
+
* @returns the disposer removing styles, listeners, and observers.
|
|
2134
|
+
*/
|
|
2135
|
+
function setupSettingsNavScroll(_ctx) {
|
|
2136
|
+
const previous = getGlobalCleanup();
|
|
2137
|
+
if (typeof previous === 'function') {
|
|
2138
|
+
previous();
|
|
2139
|
+
setGlobalCleanup(undefined);
|
|
2140
|
+
}
|
|
2141
|
+
const removeStyles = installNavScrollStyles();
|
|
2142
|
+
let list;
|
|
2143
|
+
let onScroll;
|
|
2144
|
+
let hideTimer;
|
|
2145
|
+
const detach = () => {
|
|
2146
|
+
if (list === undefined)
|
|
2147
|
+
return;
|
|
2148
|
+
if (onScroll !== undefined)
|
|
2149
|
+
list.removeEventListener('scroll', onScroll);
|
|
2150
|
+
list.classList.remove(SHOW_CLASS);
|
|
2151
|
+
if (hideTimer !== undefined) {
|
|
2152
|
+
window.clearTimeout(hideTimer);
|
|
2153
|
+
hideTimer = undefined;
|
|
2154
|
+
}
|
|
2155
|
+
list = undefined;
|
|
2156
|
+
onScroll = undefined;
|
|
2157
|
+
};
|
|
2158
|
+
/** Fade the thumb out; the scroll listener stays attached for the next scroll. */
|
|
2159
|
+
const hide = () => {
|
|
2160
|
+
if (list === undefined)
|
|
2161
|
+
return;
|
|
2162
|
+
list.classList.remove(SHOW_CLASS);
|
|
2163
|
+
hideTimer = undefined;
|
|
2164
|
+
};
|
|
2165
|
+
const show = () => {
|
|
2166
|
+
if (list === undefined)
|
|
2167
|
+
return;
|
|
2168
|
+
list.classList.add(SHOW_CLASS);
|
|
2169
|
+
if (hideTimer !== undefined)
|
|
2170
|
+
window.clearTimeout(hideTimer);
|
|
2171
|
+
hideTimer = window.setTimeout(hide, HIDE_DELAY_MS);
|
|
2172
|
+
};
|
|
2173
|
+
const attach = (next) => {
|
|
2174
|
+
detach();
|
|
2175
|
+
list = next;
|
|
2176
|
+
onScroll = show;
|
|
2177
|
+
list.addEventListener('scroll', onScroll, { passive: true });
|
|
2178
|
+
};
|
|
2179
|
+
const scan = () => {
|
|
2180
|
+
const found = document.querySelector(NAV_LIST_SELECTOR);
|
|
2181
|
+
if (found === list)
|
|
2182
|
+
return;
|
|
2183
|
+
if (found === null) {
|
|
2184
|
+
detach();
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
attach(found);
|
|
2188
|
+
};
|
|
2189
|
+
let frame = 0;
|
|
2190
|
+
const scheduleScan = () => {
|
|
2191
|
+
if (frame !== 0)
|
|
2192
|
+
return;
|
|
2193
|
+
frame = window.requestAnimationFrame(() => {
|
|
2194
|
+
frame = 0;
|
|
2195
|
+
scan();
|
|
2196
|
+
});
|
|
2197
|
+
};
|
|
2198
|
+
const observer = new MutationObserver(scheduleScan);
|
|
2199
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
2200
|
+
scan();
|
|
2201
|
+
const cleanup = () => {
|
|
2202
|
+
observer.disconnect();
|
|
2203
|
+
if (frame !== 0)
|
|
2204
|
+
window.cancelAnimationFrame(frame);
|
|
2205
|
+
detach();
|
|
2206
|
+
removeStyles();
|
|
2207
|
+
setGlobalCleanup(undefined);
|
|
2208
|
+
};
|
|
2209
|
+
setGlobalCleanup(cleanup);
|
|
2210
|
+
return cleanup;
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
__modules["./tweaks/stable-table.js"] = function(module, exports, require, __load_) {
|
|
2214
|
+
"use strict";
|
|
2215
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2216
|
+
exports.STABLE_TABLE_CSS_ID = void 0;
|
|
2217
|
+
exports.injectStableTableStyles = injectStableTableStyles;
|
|
2218
|
+
/**
|
|
2219
|
+
* dsh-style-tweaks — stable-table tweak.
|
|
2220
|
+
*
|
|
2221
|
+
* Locks markdown-table layout on hover so surrounding content does not
|
|
2222
|
+
* reflow ("text jumps when I hover a table").
|
|
2223
|
+
*
|
|
2224
|
+
* DSH web renders markdown tables inside a `.md-table-wide` scroll wrapper.
|
|
2225
|
+
* On hover, DSH's own stylesheet sets `padding-bottom: 0` on that wrapper
|
|
2226
|
+
* to expand the scroll viewport — but the resulting height delta (~8 px)
|
|
2227
|
+
* pushes every sibling below the table, which is the user-visible "jump".
|
|
2228
|
+
* DSH does not change the inner cells' geometry on hover.
|
|
2229
|
+
*
|
|
2230
|
+
* Diagnosed against DSH's own stylesheet (verified in a live browser):
|
|
2231
|
+
* ._tableScroll_*.md-table-wide:hover { padding-bottom: 0; }
|
|
2232
|
+
*
|
|
2233
|
+
* Fix: pin the wrapper's padding-bottom to its base value (8 px) across
|
|
2234
|
+
* base + hover + focus-visible states, so the table's outer height never
|
|
2235
|
+
* changes. As belt-and-braces, also suppress non-background transitions in
|
|
2236
|
+
* case future DSH versions add inner-cell hover effects.
|
|
2237
|
+
*
|
|
2238
|
+
* Selectors use stable hooks only — `.md-table-wide` is a hand-written
|
|
2239
|
+
* class DSH reserves for plugins/extensions; CSS Modules-generated names
|
|
2240
|
+
* like `_tableScroll_177e0_174` are intentionally avoided.
|
|
2241
|
+
*/
|
|
2242
|
+
const STABLE_TABLE_CSS = `
|
|
2243
|
+
/* Pin the wrapper's padding-bottom to its base value across every state, so
|
|
2244
|
+
* the wrapper's height never changes on hover. 8 px is the DSH baseline
|
|
2245
|
+
* (verified in a live browser via getComputedStyle on .md-table-wide).
|
|
2246
|
+
*
|
|
2247
|
+
* Specificity note: DSH's own rule is something like
|
|
2248
|
+
* .[css-modules].md-table-wide:hover, ...:focus-visible { padding-bottom: 0 }
|
|
2249
|
+
* which has specificity (0, 0, 3, 0). A plain .md-table-wide:hover would only
|
|
2250
|
+
* be (0, 0, 2, 0), so even with !important DSH wins on the specificity
|
|
2251
|
+
* tiebreak. We double-up the .md-table-wide class to match (and exceed) DSH's
|
|
2252
|
+
* specificity. */
|
|
2253
|
+
.md-table-wide,
|
|
2254
|
+
.md-table-wide.md-table-wide:hover,
|
|
2255
|
+
.md-table-wide.md-table-wide:focus,
|
|
2256
|
+
.md-table-wide.md-table-wide:focus-visible,
|
|
2257
|
+
.md-table-wide.md-table-wide:focus-within {
|
|
2258
|
+
padding-bottom: 8px !important;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/* Belt-and-braces: no animated transitions on geometry properties — even
|
|
2262
|
+
* if DSH sets transition: all 0.15s, the value oscillates within a zero
|
|
2263
|
+
* range so the user never sees motion. Background-color / color keep
|
|
2264
|
+
* transitioning for any highlight effect. */
|
|
2265
|
+
.md-table-wide table * {
|
|
2266
|
+
transition-property: background-color, color !important;
|
|
2267
|
+
}
|
|
2268
|
+
`;
|
|
2269
|
+
exports.STABLE_TABLE_CSS_ID = 'cst-stable-table';
|
|
2270
|
+
function injectStableTableStyles() {
|
|
2271
|
+
let style = document.querySelector(`style[data-tweak-css="${exports.STABLE_TABLE_CSS_ID}"]`);
|
|
2272
|
+
if (style === null) {
|
|
2273
|
+
style = document.createElement('style');
|
|
2274
|
+
style.dataset.tweak = 'cst';
|
|
2275
|
+
style.dataset.tweakCss = exports.STABLE_TABLE_CSS_ID;
|
|
2276
|
+
style.textContent = STABLE_TABLE_CSS;
|
|
2277
|
+
document.head.appendChild(style);
|
|
2278
|
+
}
|
|
2279
|
+
return () => { style?.remove(); };
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
__modules["./tweaks/stable-turn-rail.js"] = function(module, exports, require, __load_) {
|
|
2283
|
+
"use strict";
|
|
2284
|
+
/**
|
|
2285
|
+
* dsh-style-tweaks — stable-turn-rail tweak.
|
|
2286
|
+
*
|
|
2287
|
+
* Fixes the visual jump of the turn-navigation rail when the reader scrolls
|
|
2288
|
+
* the chat upward into the system-prompt area at the very top of the
|
|
2289
|
+
* conversation column.
|
|
2290
|
+
*
|
|
2291
|
+
* ## Symptom
|
|
2292
|
+
*
|
|
2293
|
+
* The rail (right-side Turn navigator) sits inside the chat scroll container
|
|
2294
|
+
* as a `position: sticky; top: 0; height: 0` slot. Its parent has
|
|
2295
|
+
* `padding-top: 16px`. With `top: 0` sticky behaviour, the slot's natural
|
|
2296
|
+
* flow position is `padding-top` below the scrollport's visible top, so:
|
|
2297
|
+
*
|
|
2298
|
+
* • When the chat is scrolled far enough that the slot would naturally
|
|
2299
|
+
* be above the scrollport top (scrollTop > 16px), sticky activates and
|
|
2300
|
+
* pins the slot at scrollport top:0. Rail sits at the expected height.
|
|
2301
|
+
* • When the chat is scrolled all the way up (scrollTop ≤ 16px), the slot
|
|
2302
|
+
* falls back to its natural position — which is 16px below scrollport
|
|
2303
|
+
* top. The rail shifts DOWN by 16px.
|
|
2304
|
+
*
|
|
2305
|
+
* After clicking the first Turn marker, the chat lands such that the first
|
|
2306
|
+
* message sits near the reading line but the chat can still scroll up into
|
|
2307
|
+
* the system prompt above it. Scrolling that last bit causes the rail to
|
|
2308
|
+
* visibly drop by 16px.
|
|
2309
|
+
*
|
|
2310
|
+
* ## Fix
|
|
2311
|
+
*
|
|
2312
|
+
* Move the 16px of top padding from the scroll container down into the
|
|
2313
|
+
* chat column (`[data-chat-flow]`). Net effect:
|
|
2314
|
+
*
|
|
2315
|
+
* • Container's `padding-top` becomes 0, so the sticky slot's natural
|
|
2316
|
+
* position is at scrollport top:0 even when scrollTop = 0.
|
|
2317
|
+
* • Column picks up `padding-top: 16px`, so the first message still
|
|
2318
|
+
* starts 16px below the scrollport top — identical visual layout to
|
|
2319
|
+
* before, only the source of those 16px has moved.
|
|
2320
|
+
*
|
|
2321
|
+
* Selectors use stable DSH markers only:
|
|
2322
|
+
*
|
|
2323
|
+
* • `[data-chat-flow]` — the chat column (stable attribute, hardcoded in
|
|
2324
|
+
* ChatView.tsx).
|
|
2325
|
+
* • `:has(> * > nav[aria-label="Turn navigation"], > * > nav[aria-label="轮次导航"])`
|
|
2326
|
+
* — the chat scroll container, identified as the only element that has
|
|
2327
|
+
* the rail nav as a grandchild and the chat column as a direct child.
|
|
2328
|
+
* The two aria-label values cover DSH's shipped en / zh dictionaries
|
|
2329
|
+
* (TurnNavigator.tsx → `t('chat.turnNavigation.label')`).
|
|
2330
|
+
*
|
|
2331
|
+
* No CSS-module class names are referenced, so the fix stays correct across
|
|
2332
|
+
* DSH builds that re-hash module class names.
|
|
2333
|
+
*/
|
|
2334
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2335
|
+
exports.STABLE_TURN_RAIL_CSS_ID = void 0;
|
|
2336
|
+
exports.injectStableTurnRailStyles = injectStableTurnRailStyles;
|
|
2337
|
+
const STABLE_TURN_RAIL_CSS = `
|
|
2338
|
+
/* The chat scroll container: identifiable as the parent of the chat column
|
|
2339
|
+
that also holds the turn-rail slot as a grandchild. Removing its top
|
|
2340
|
+
padding keeps the sticky slot's natural position aligned with the
|
|
2341
|
+
scrollport's visible top, so the rail never "drops" near scrollTop = 0. */
|
|
2342
|
+
:has(> * > nav[aria-label="Turn navigation"], > * > nav[aria-label="轮次导航"]):has(> [data-chat-flow]) {
|
|
2343
|
+
padding-top: 0 !important;
|
|
2344
|
+
}
|
|
2345
|
+
/* Push the 16px that used to live on the container down into the chat
|
|
2346
|
+
column, so the first message still starts 16px below the scrollport top
|
|
2347
|
+
(identical to the unmodified layout). */
|
|
2348
|
+
[data-chat-flow] {
|
|
2349
|
+
padding-top: 16px !important;
|
|
2350
|
+
}
|
|
2351
|
+
`;
|
|
2352
|
+
exports.STABLE_TURN_RAIL_CSS_ID = 'cst-stable-turn-rail';
|
|
2353
|
+
function injectStableTurnRailStyles() {
|
|
2354
|
+
let style = document.querySelector(`style[data-tweak-css="${exports.STABLE_TURN_RAIL_CSS_ID}"]`);
|
|
2355
|
+
if (style === null) {
|
|
2356
|
+
style = document.createElement('style');
|
|
2357
|
+
style.dataset.tweak = 'cst';
|
|
2358
|
+
style.dataset.tweakCss = exports.STABLE_TURN_RAIL_CSS_ID;
|
|
2359
|
+
style.textContent = STABLE_TURN_RAIL_CSS;
|
|
2360
|
+
document.head.appendChild(style);
|
|
2361
|
+
}
|
|
2362
|
+
return () => { style?.remove(); };
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
function __resolve(from, request) {
|
|
2366
|
+
if (!request.startsWith(".")) return request;
|
|
2367
|
+
var parts = from.slice(2).split("/"); parts.pop();
|
|
2368
|
+
for (var part of request.split("/")) { if (part === "." || part === "") continue; if (part === "..") parts.pop(); else parts.push(part); }
|
|
2369
|
+
return "./" + parts.join("/");
|
|
2370
|
+
}
|
|
2371
|
+
function __load(id) {
|
|
2372
|
+
if (__modules[id] === undefined) return require(id);
|
|
2373
|
+
if (__cache[id] !== undefined) return __cache[id].exports;
|
|
2374
|
+
var module = __cache[id] = { exports: {} };
|
|
2375
|
+
__modules[id](module, module.exports, require, function(request) { var resolved = __resolve(id, request); return __modules[resolved] === undefined ? require(request) : __load(resolved); });
|
|
2376
|
+
return module.exports;
|
|
2377
|
+
}
|
|
2378
|
+
return __load("./index.js"); } });
|
|
2379
|
+
//# sourceMappingURL=client.js.map
|