deepseek-harness-background 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/README.zh.md +11 -3
- package/lib/client.js +1264 -101
- package/lib/client.js.map +1 -1
- package/lib/index.js +78 -31
- package/package.json +86 -88
package/lib/client.js
CHANGED
|
@@ -4,28 +4,84 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
7
29
|
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
8
31
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
|
+
let react_dom = require("react-dom");
|
|
33
|
+
react_dom = __toESM(react_dom, 1);
|
|
9
34
|
//#region src/client/background-css.ts
|
|
10
35
|
/**
|
|
11
36
|
* Background CSS — the stylesheet injected once into the document when the
|
|
12
37
|
* plugin first runs. It defines the behind-body wallpaper layer, the scrim,
|
|
13
|
-
* and the frosted-glass
|
|
14
|
-
* onto `document.body`. Kept in a single idempotent
|
|
15
|
-
* tag (mirrors the reference `dsh-wallpaper-engine`
|
|
38
|
+
* and the frosted-glass surfaces, all driven by CSS variables the painter
|
|
39
|
+
* writes onto `document.body`. Kept in a single idempotent
|
|
40
|
+
* `<style data-plugin-css>` tag (mirrors the reference `dsh-wallpaper-engine`
|
|
41
|
+
* bundle).
|
|
16
42
|
*/
|
|
17
43
|
/** Unique id stamped on the injected style element (dedup key). */
|
|
18
44
|
const BACKGROUND_CSS_TAG = "deepseek-harness-background/styles";
|
|
19
45
|
/** Body attribute the painter sets while a wallpaper is active. */
|
|
20
46
|
const ACTIVE_ATTR = "data-dsh-bg";
|
|
21
47
|
/**
|
|
48
|
+
* Body attribute the painter sets only while the glass is actually on (a
|
|
49
|
+
* wallpaper is active AND panelOpacity < 1). The explicit-fill rules below —
|
|
50
|
+
* whitelisted buttons, popovers and badges whose official fills are not
|
|
51
|
+
* token-mediated — key on this gate so they return to the exact official
|
|
52
|
+
* paints when the user maxes the panel-opacity slider.
|
|
53
|
+
*/
|
|
54
|
+
const GLASS_ATTR = "data-dsh-bg-glass";
|
|
55
|
+
/**
|
|
22
56
|
* The backdrop CSS. The wallpaper is a fixed `z-index:-2` element (below the
|
|
23
|
-
* scrim at `-1`, below the UI). While active, the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
57
|
+
* scrim at `-1`, below the UI). While active, the app frame + sidebar fills
|
|
58
|
+
* turn transparent so all columns share one backdrop.
|
|
59
|
+
*
|
|
60
|
+
* Frosted glass is a WHITELIST, not a blanket: only the surfaces that float
|
|
61
|
+
* as small cards/buttons over the wallpaper are glassed — composer card,
|
|
62
|
+
* message bubbles, code surfaces (blocks, inline code, tool IO cards,
|
|
63
|
+
* skill/MCP call cards), the three chrome buttons (new session, composer +
|
|
64
|
+
* button, scroll-to-bottom), the agent task strip family (`
|
|
65
|
+
* --dsw-specific-tip`), the subagent lineage popover, the home hero
|
|
66
|
+
* "preview" badge, and the timeline rail (timeline-css.ts). Every glassed
|
|
67
|
+
* surface shares ONE recipe: fill from the painter's --dsw-specific-input-
|
|
68
|
+
* major token (same alpha curve + theme dimming), the full shared
|
|
69
|
+
* blur/saturate/brightness chain driven by the glass-blur slider, and the
|
|
70
|
+
* shared sheen vars — so no surface reads heavier than the composer card.
|
|
71
|
+
* Dialogs, the settings UI, menus, tooltips, toasts and every hover/accent
|
|
72
|
+
* fill keep the OFFICIAL opaque paints — they are reading surfaces and must
|
|
73
|
+
* stay legible.
|
|
74
|
+
*
|
|
75
|
+
* Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
|
|
76
|
+
* `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
|
|
77
|
+
* SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions:
|
|
78
|
+
* `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
|
|
79
|
+
* `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
|
|
80
|
+
* it), `_newSession` excludes `Label` (the button's inner label span) and
|
|
81
|
+
* `_toBottom` excludes `Slot` (the zero-height sticky wrapper) — both share
|
|
82
|
+
* the substring with the chrome control but must keep their own paint, and
|
|
83
|
+
* the subagent popover is `role="tree"` + `_menu` so the generic menu
|
|
84
|
+
* surface stays untouched.
|
|
29
85
|
*/
|
|
30
86
|
const BACKGROUND_CSS = `
|
|
31
87
|
.dsh-bg-layer {
|
|
@@ -58,22 +114,118 @@ window.__ModuleLoader__.load({
|
|
|
58
114
|
--dsw-specific-sidebar-fill: transparent;
|
|
59
115
|
}
|
|
60
116
|
|
|
61
|
-
/*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
/* ---- Whitelisted frosted-glass sheet ---------------------------------
|
|
118
|
+
Fills come from the --dsw-* tokens the painter overrides on body ONLY for
|
|
119
|
+
this list's own surfaces (input/bubble/code/tip); this block adds the
|
|
120
|
+
shared wet-glass sheen + blur from --bg-glass-blur. Exposure is calibrated
|
|
121
|
+
per scheme (the painter writes --bg-glass-brightness / --bg-glass-sheen):
|
|
122
|
+
light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
|
|
123
|
+
GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
|
|
124
|
+
slider returns these surfaces to the exact official paints. */
|
|
125
|
+
body[data-dsh-bg-glass] [data-composer-card],
|
|
126
|
+
body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
|
|
127
|
+
body[data-dsh-bg-glass] .md-code-block,
|
|
128
|
+
body[data-dsh-bg-glass] [data-terminal],
|
|
129
|
+
body[data-dsh-bg-glass] [data-diff],
|
|
130
|
+
body[data-dsh-bg-glass] [data-read],
|
|
131
|
+
body[data-dsh-bg-glass] [data-search],
|
|
132
|
+
body[data-dsh-bg-glass] [data-web],
|
|
133
|
+
body[data-dsh-bg-glass] [class*="_ioCard"],
|
|
134
|
+
body[data-dsh-bg-glass] [class*="_instructionsCard"],
|
|
135
|
+
body[data-dsh-bg-glass] [class*="_markdown"] :not(pre) > code {
|
|
136
|
+
background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01));
|
|
137
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
138
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
72
139
|
box-shadow:
|
|
73
|
-
inset 0 1px 0 rgba(255, 255, 255,
|
|
140
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
74
141
|
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
75
142
|
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
76
|
-
0 12px 40px rgba(0, 0, 0,
|
|
143
|
+
0 12px 40px rgba(0, 0, 0, 0.12);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* The sticky code-block header officially occludes scrolled code with
|
|
147
|
+
bg-base (transparent under a wallpaper): refill it with the banner token
|
|
148
|
+
(itself translucent glass) so code stays hidden beneath the sticky bar. */
|
|
149
|
+
body[data-dsh-bg] .md-code-block [class*="_bannerWrap"] {
|
|
150
|
+
background-color: var(--dsw-alias-markdown-code-block-banner);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* The sidebar build badge paints with the label-ink token — a TEXT token we
|
|
154
|
+
must not override globally — so it gets a theme-aware translucent ink. */
|
|
155
|
+
body[data-dsh-bg] [class*="_buildRevision"] {
|
|
156
|
+
background-color: rgba(15, 17, 21, 0.62);
|
|
157
|
+
}
|
|
158
|
+
body[data-ds-dark-theme][data-dsh-bg] [class*="_buildRevision"] {
|
|
159
|
+
background-color: rgba(249, 250, 251, 0.62);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* The empty-state hero glow asset would wash out the wallpaper behind the
|
|
163
|
+
welcome message — dim it so the art stays visible. */
|
|
164
|
+
body[data-dsh-bg] [class*="_heroGlow"] {
|
|
165
|
+
opacity: 0.2;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ---- Whitelisted chrome buttons --------------------------------------
|
|
169
|
+
These three buttons' official fills ride button-* tokens shared with
|
|
170
|
+
unrelated surfaces, so instead of overriding the tokens they get explicit
|
|
171
|
+
paints — but the SAME glass recipe as the composer card and bubbles:
|
|
172
|
+
fill = the painter's --dsw-specific-input-major token (the identical
|
|
173
|
+
translucent white + theme curve), filter = the sheet's exact
|
|
174
|
+
blur/saturate/brightness chain (blur follows the glass-blur slider, no
|
|
175
|
+
caps), sheen = the shared sheen vars. Hover = the same white glass at the
|
|
176
|
+
boosted alpha var. Gated on data-dsh-bg-glass (off when the panel is
|
|
177
|
+
fully opaque). */
|
|
178
|
+
body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]),
|
|
179
|
+
body[data-dsh-bg-glass] [data-composer-card] [class*="_add"],
|
|
180
|
+
body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]) {
|
|
181
|
+
background-color: var(--dsw-specific-input-major);
|
|
182
|
+
background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01));
|
|
183
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
184
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
185
|
+
box-shadow:
|
|
186
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
187
|
+
inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
|
|
188
|
+
0 4px 14px rgba(0, 0, 0, 0.08);
|
|
189
|
+
}
|
|
190
|
+
body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]):hover,
|
|
191
|
+
body[data-dsh-bg-glass] [data-composer-card] [class*="_add"]:hover:not(:disabled),
|
|
192
|
+
body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]):hover {
|
|
193
|
+
background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
|
|
194
|
+
}
|
|
195
|
+
/* Collapsed rail renders the new-session control as a bare icon — keep the
|
|
196
|
+
official transparent paint (and its official translucent hover). */
|
|
197
|
+
body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"],
|
|
198
|
+
body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"]:hover {
|
|
199
|
+
background-color: transparent;
|
|
200
|
+
background-image: none;
|
|
201
|
+
box-shadow: none;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ---- Subagent lineage popover (title-bar expanded list) --------------
|
|
205
|
+
Its official fill rides the menu token shared with every dropdown; glass
|
|
206
|
+
THIS popover explicitly via its role="tree" + _menu combination so real
|
|
207
|
+
menus stay officially opaque and legible. Same unified recipe: the
|
|
208
|
+
composer fill token + the full shared filter chain (the official
|
|
209
|
+
elevation shadow stays). */
|
|
210
|
+
body[data-dsh-bg-glass] [role="tree"][class*="_menu"] {
|
|
211
|
+
background-color: var(--dsw-specific-input-major);
|
|
212
|
+
background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01));
|
|
213
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
214
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ---- Home hero "preview" badge (top-right superscript pill) ----------
|
|
218
|
+
Officially the state-business-tertiary pastel; re-emit the same hue as
|
|
219
|
+
glass but with the UNIFIED alpha curve (color-mix against the painter's
|
|
220
|
+
--bg-glass-alpha) and the shared blur chain, so it sits on the wallpaper
|
|
221
|
+
with exactly the glass strength of the composer card. */
|
|
222
|
+
body[data-dsh-bg-glass] [class*="_previewBadge"] {
|
|
223
|
+
background-color: color-mix(in srgb, rgb(228 237 253) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
|
|
224
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
225
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
226
|
+
}
|
|
227
|
+
body[data-ds-dark-theme][data-dsh-bg-glass] [class*="_previewBadge"] {
|
|
228
|
+
background-color: color-mix(in srgb, rgb(52 65 91) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
|
|
77
229
|
}
|
|
78
230
|
`;
|
|
79
231
|
/**
|
|
@@ -92,8 +244,6 @@ window.__ModuleLoader__.load({
|
|
|
92
244
|
//#region src/settings.ts
|
|
93
245
|
/** Browser-facing base path of the background API (shared with the host routes). */
|
|
94
246
|
const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
|
|
95
|
-
/** Maximum scrim opacity (a nearly opaque veil keeps text legible over any art). */
|
|
96
|
-
const SCRIM_MAX = .95;
|
|
97
247
|
/** Default scrim strength applied over user art (a light readability veil). */
|
|
98
248
|
const DEFAULT_SCRIM = .25;
|
|
99
249
|
/** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
|
|
@@ -119,10 +269,90 @@ window.__ModuleLoader__.load({
|
|
|
119
269
|
const LAYER_CLASS = "dsh-bg-layer";
|
|
120
270
|
const IMAGE_CLASS = "dsh-bg-image";
|
|
121
271
|
const SCRIM_CLASS = "dsh-bg-scrim";
|
|
122
|
-
/**
|
|
123
|
-
|
|
272
|
+
/**
|
|
273
|
+
* White-glass tokens — the WHITELISTED surfaces only. Everything that floats
|
|
274
|
+
* as a small card over the wallpaper: the composer input stack, message
|
|
275
|
+
* bubbles, the markdown code surfaces (blocks + banner + inline code), and
|
|
276
|
+
* the agent task strip family (`--dsw-specific-tip`: TodoPanel + GoalBar +
|
|
277
|
+
* QueueDock share one dock column and one fill token). Menus, dialogs, the
|
|
278
|
+
* settings UI, tooltips, toasts, hover fills and every accent keep the
|
|
279
|
+
* OFFICIAL token values — they are reading surfaces and must stay legible.
|
|
280
|
+
* `factor` scales the alpha relative to the input surface.
|
|
281
|
+
*/
|
|
282
|
+
const GLASS_SURFACE_TOKENS = [
|
|
283
|
+
{
|
|
284
|
+
token: "--dsw-specific-input-major",
|
|
285
|
+
factor: 1
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
token: "--dsw-specific-bubble",
|
|
289
|
+
factor: .8
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
token: "--dsw-alias-markdown-code-block",
|
|
293
|
+
factor: 1
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
token: "--dsw-alias-markdown-code-block-banner",
|
|
297
|
+
factor: 1
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
token: "--dsw-alias-markdown-inline-code",
|
|
301
|
+
factor: .9
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
token: "--dsw-specific-tip",
|
|
305
|
+
factor: 1
|
|
306
|
+
}
|
|
307
|
+
];
|
|
308
|
+
/** Every token the glass owns (save/restore key set). */
|
|
309
|
+
const GLASS_TOKENS = GLASS_SURFACE_TOKENS.map(({ token }) => token);
|
|
124
310
|
/** Fraction of the official white surface alpha kept at the most transparent panel setting. */
|
|
125
311
|
const GLASS_MIN_ALPHA = .05;
|
|
312
|
+
/** Base white-glass alpha for one panelOpacity (the curve every surface shares). */
|
|
313
|
+
function glassAlpha(panelOpacity) {
|
|
314
|
+
const alpha = GLASS_MIN_ALPHA + panelOpacity * .85;
|
|
315
|
+
return Math.max(0, Math.min(.9, alpha));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Saturation boost that rides the blur radius. Gentler than the reference
|
|
319
|
+
* engine (1.1 + blur * 0.02, capped at 1.6): stacking a hot saturate on top of
|
|
320
|
+
* the brightness/sheen layers pushed bright wallpapers into clipping.
|
|
321
|
+
*/
|
|
322
|
+
function glassSaturate(blur) {
|
|
323
|
+
if (blur <= 0) return 1;
|
|
324
|
+
return Math.round(Math.min(1.6, 1.1 + blur * .02) * 1e3) / 1e3;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Exposure calibration per scheme. Light glass neither boosts brightness nor
|
|
328
|
+
* stacks a heavy sheen (mainstream frosted recipes — macOS vibrancy, Windows
|
|
329
|
+
* acrylic, common web glass — blur + saturate + tint without a positive
|
|
330
|
+
* brightness gain); dark glass keeps the reference engine's slight lift, which
|
|
331
|
+
* stops the white-glass fills from turning muddy.
|
|
332
|
+
*/
|
|
333
|
+
function glassExposure(inDark) {
|
|
334
|
+
return inDark ? {
|
|
335
|
+
brightness: "1.04",
|
|
336
|
+
sheen: "0.16",
|
|
337
|
+
sheenMid: "0.05"
|
|
338
|
+
} : {
|
|
339
|
+
brightness: "0.98",
|
|
340
|
+
sheen: "0.07",
|
|
341
|
+
sheenMid: "0.02"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The theme-aware glass surface alphas for one panelOpacity: the input-surface
|
|
346
|
+
* token and the bubble token share one curve, dimmed per scheme. Shared by the
|
|
347
|
+
* live painter and the settings-row preview card so the two cannot drift.
|
|
348
|
+
*/
|
|
349
|
+
function glassSurfaceAlphas(panelOpacity, inDark) {
|
|
350
|
+
const alpha = glassAlpha(panelOpacity) * (inDark ? .4 : .8);
|
|
351
|
+
return {
|
|
352
|
+
major: `rgba(255, 255, 255, ${alpha.toFixed(3)})`,
|
|
353
|
+
bubble: `rgba(255, 255, 255, ${(alpha * .8).toFixed(3)})`
|
|
354
|
+
};
|
|
355
|
+
}
|
|
126
356
|
/**
|
|
127
357
|
* Resolve the effective background image url for a section.
|
|
128
358
|
* @param settings - resolved background section.
|
|
@@ -148,7 +378,7 @@ window.__ModuleLoader__.load({
|
|
|
148
378
|
rememberOnce(prop) {
|
|
149
379
|
if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
|
|
150
380
|
const value = document.body.style.getPropertyValue(prop);
|
|
151
|
-
if (
|
|
381
|
+
if (GLASS_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
|
|
152
382
|
else this.savedVars.set(prop, value);
|
|
153
383
|
}
|
|
154
384
|
/**
|
|
@@ -169,6 +399,8 @@ window.__ModuleLoader__.load({
|
|
|
169
399
|
document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
|
|
170
400
|
const url = backgroundImageUrl(settings, uploadBase);
|
|
171
401
|
if (url === "") {
|
|
402
|
+
this.observer?.disconnect();
|
|
403
|
+
this.observer = void 0;
|
|
172
404
|
this.applySurfaceGlass(settings, true);
|
|
173
405
|
this.active = false;
|
|
174
406
|
this.removeLayers();
|
|
@@ -181,10 +413,11 @@ window.__ModuleLoader__.load({
|
|
|
181
413
|
this.img = document.createElement("img");
|
|
182
414
|
this.img.className = IMAGE_CLASS;
|
|
183
415
|
this.img.alt = "";
|
|
416
|
+
this.img.referrerPolicy = "no-referrer";
|
|
184
417
|
this.layer.appendChild(this.img);
|
|
185
418
|
document.body.appendChild(this.layer);
|
|
186
419
|
}
|
|
187
|
-
if (this.img && this.img.src !== url) this.img.src = url;
|
|
420
|
+
if (this.img && this.img.getAttribute("src") !== url) this.img.src = url;
|
|
188
421
|
if (!this.scrim) {
|
|
189
422
|
this.scrim = document.createElement("div");
|
|
190
423
|
this.scrim.className = SCRIM_CLASS;
|
|
@@ -199,7 +432,7 @@ window.__ModuleLoader__.load({
|
|
|
199
432
|
this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
|
|
200
433
|
this.setVar("--bg-scrim", String(settings.scrim));
|
|
201
434
|
this.setVar("--bg-glass-blur", `${settings.blur}px`);
|
|
202
|
-
this.setVar("--bg-glass-saturate", String(
|
|
435
|
+
this.setVar("--bg-glass-saturate", String(glassSaturate(settings.blur)));
|
|
203
436
|
this.applySurfaceGlass(settings, false);
|
|
204
437
|
if (!this.observer) {
|
|
205
438
|
this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
|
|
@@ -232,7 +465,7 @@ window.__ModuleLoader__.load({
|
|
|
232
465
|
break;
|
|
233
466
|
case "blur":
|
|
234
467
|
this.setVar("--bg-glass-blur", `${value}px`);
|
|
235
|
-
this.setVar("--bg-glass-saturate", String(
|
|
468
|
+
this.setVar("--bg-glass-saturate", String(glassSaturate(Number(value))));
|
|
236
469
|
break;
|
|
237
470
|
case "wallpaperBlur":
|
|
238
471
|
this.setVar("--bg-wallpaper-blur", `${value}px`);
|
|
@@ -251,9 +484,9 @@ window.__ModuleLoader__.load({
|
|
|
251
484
|
applySurfaceGlass(settings, forceRestore) {
|
|
252
485
|
const s = document.body.style;
|
|
253
486
|
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
254
|
-
for (const token of
|
|
487
|
+
for (const token of GLASS_TOKENS) this.rememberOnce(token);
|
|
255
488
|
const restore = () => {
|
|
256
|
-
for (const token of
|
|
489
|
+
for (const token of GLASS_TOKENS) {
|
|
257
490
|
const original = this.savedTokens.get(token);
|
|
258
491
|
if (original !== void 0 && original !== "") s.setProperty(token, original);
|
|
259
492
|
else s.removeProperty(token);
|
|
@@ -261,14 +494,22 @@ window.__ModuleLoader__.load({
|
|
|
261
494
|
};
|
|
262
495
|
if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
|
|
263
496
|
restore();
|
|
497
|
+
document.body.removeAttribute(GLASS_ATTR);
|
|
264
498
|
this.setVar("--bg-glass-blur", "0px");
|
|
499
|
+
this.setVar("--bg-glass-brightness", "1");
|
|
500
|
+
s.removeProperty("--bg-glass-alpha");
|
|
501
|
+
s.removeProperty("--bg-glass-alpha-strong");
|
|
265
502
|
return;
|
|
266
503
|
}
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
504
|
+
document.body.setAttribute(GLASS_ATTR, "on");
|
|
505
|
+
const exposure = glassExposure(inDark);
|
|
506
|
+
this.setVar("--bg-glass-brightness", exposure.brightness);
|
|
507
|
+
this.setVar("--bg-glass-sheen", exposure.sheen);
|
|
508
|
+
this.setVar("--bg-glass-sheen-mid", exposure.sheenMid);
|
|
509
|
+
const base = glassAlpha(settings.panelOpacity) * (inDark ? .4 : .8);
|
|
510
|
+
for (const { token, factor } of GLASS_SURFACE_TOKENS) s.setProperty(token, `rgba(255, 255, 255, ${(base * factor).toFixed(3)})`);
|
|
511
|
+
this.setVar("--bg-glass-alpha", base.toFixed(3));
|
|
512
|
+
this.setVar("--bg-glass-alpha-strong", Math.min(.9, base * 1.5).toFixed(3));
|
|
272
513
|
}
|
|
273
514
|
/** Remove the wallpaper + scrim layers and clear the active attribute. */
|
|
274
515
|
removeLayers() {
|
|
@@ -289,6 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
289
530
|
this.removeLayers();
|
|
290
531
|
const s = document.body.style;
|
|
291
532
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
533
|
+
document.body.removeAttribute(GLASS_ATTR);
|
|
292
534
|
for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
|
|
293
535
|
for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
|
|
294
536
|
this.savedTokens.clear();
|
|
@@ -330,6 +572,7 @@ window.__ModuleLoader__.load({
|
|
|
330
572
|
"--bg-wallpaper-scale",
|
|
331
573
|
"--bg-glass-blur",
|
|
332
574
|
"--bg-glass-saturate",
|
|
575
|
+
"--bg-glass-brightness",
|
|
333
576
|
"--bg-preview-glass"
|
|
334
577
|
];
|
|
335
578
|
/**
|
|
@@ -349,15 +592,14 @@ window.__ModuleLoader__.load({
|
|
|
349
592
|
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
350
593
|
if (settings.panelOpacity >= 1) {
|
|
351
594
|
s.setProperty("--bg-glass-blur", "0px");
|
|
595
|
+
s.setProperty("--bg-glass-brightness", "1");
|
|
352
596
|
s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
|
|
353
597
|
return;
|
|
354
598
|
}
|
|
355
599
|
s.setProperty("--bg-glass-blur", `${settings.blur}px`);
|
|
356
|
-
s.setProperty("--bg-glass-saturate", String(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const factor = inDark ? .4 : .8;
|
|
360
|
-
s.setProperty("--bg-preview-glass", `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`);
|
|
600
|
+
s.setProperty("--bg-glass-saturate", String(glassSaturate(settings.blur)));
|
|
601
|
+
s.setProperty("--bg-glass-brightness", glassExposure(inDark).brightness);
|
|
602
|
+
s.setProperty("--bg-preview-glass", glassSurfaceAlphas(settings.panelOpacity, inDark).bubble);
|
|
361
603
|
}
|
|
362
604
|
/** Remove every preview variable from an element (restores CSS defaults). */
|
|
363
605
|
function clearPreviewSurface(el) {
|
|
@@ -371,8 +613,9 @@ window.__ModuleLoader__.load({
|
|
|
371
613
|
"background.description": "为整个应用界面设置背景图片,可调不透明度、遮罩、面板透明与毛玻璃模糊",
|
|
372
614
|
"background.upload": "上传图片",
|
|
373
615
|
"background.uploading": "上传中…",
|
|
374
|
-
"background.
|
|
616
|
+
"background.uploadMediaTypes": "支持 JPG / PNG / WebP / GIF",
|
|
375
617
|
"background.urlPlaceholder": "或粘贴图片 http(s) 链接后回车",
|
|
618
|
+
"background.invalidUrl": "链接无效,请粘贴 http(s) 开头的图片链接",
|
|
376
619
|
"background.applyUrl": "应用链接",
|
|
377
620
|
"background.enabled": "背景已启用",
|
|
378
621
|
"background.source": "图片",
|
|
@@ -389,7 +632,16 @@ window.__ModuleLoader__.load({
|
|
|
389
632
|
"background.contain": "完整",
|
|
390
633
|
"background.clear": "清除背景",
|
|
391
634
|
"background.uploadFailed": "上传失败,请重试",
|
|
392
|
-
"background.saveFailed": "保存失败,请重试"
|
|
635
|
+
"background.saveFailed": "保存失败,请重试",
|
|
636
|
+
"background.timeline": "会话时间线",
|
|
637
|
+
"background.timelineHint": "右侧消息导航轨,悬停展开问题列表(DeepSeek 官网风格)",
|
|
638
|
+
"timeline.railLabel": "对话时间线",
|
|
639
|
+
"timeline.roleUser": "用户",
|
|
640
|
+
"timeline.noText": "(无文本内容)",
|
|
641
|
+
"timeline.marksOnly": "★ 只看标记",
|
|
642
|
+
"timeline.mark": "标记为重点",
|
|
643
|
+
"timeline.unmark": "取消重点标记",
|
|
644
|
+
"timeline.noMarks": "还没有标记的重点"
|
|
393
645
|
};
|
|
394
646
|
/** English copy. */
|
|
395
647
|
const en = {
|
|
@@ -397,8 +649,9 @@ window.__ModuleLoader__.load({
|
|
|
397
649
|
"background.description": "Set a background image behind the whole app surface with adjustable opacity, scrim, panel transparency and glass blur",
|
|
398
650
|
"background.upload": "Upload image",
|
|
399
651
|
"background.uploading": "Uploading…",
|
|
400
|
-
"background.
|
|
652
|
+
"background.uploadMediaTypes": "JPG / PNG / WebP / GIF",
|
|
401
653
|
"background.urlPlaceholder": "Or paste an image http(s) link and press Enter",
|
|
654
|
+
"background.invalidUrl": "Invalid link — paste an image link starting with http(s)",
|
|
402
655
|
"background.applyUrl": "Apply URL",
|
|
403
656
|
"background.enabled": "Background on",
|
|
404
657
|
"background.source": "Image",
|
|
@@ -415,7 +668,16 @@ window.__ModuleLoader__.load({
|
|
|
415
668
|
"background.contain": "Contain",
|
|
416
669
|
"background.clear": "Clear background",
|
|
417
670
|
"background.uploadFailed": "Upload failed, try again",
|
|
418
|
-
"background.saveFailed": "Save failed, try again"
|
|
671
|
+
"background.saveFailed": "Save failed, try again",
|
|
672
|
+
"background.timeline": "Conversation timeline",
|
|
673
|
+
"background.timelineHint": "Message rail at the right edge; hover to expand the question list (DeepSeek web style)",
|
|
674
|
+
"timeline.railLabel": "Chat timeline",
|
|
675
|
+
"timeline.roleUser": "User",
|
|
676
|
+
"timeline.noText": "(no text)",
|
|
677
|
+
"timeline.marksOnly": "★ Marked only",
|
|
678
|
+
"timeline.mark": "Mark as key point",
|
|
679
|
+
"timeline.unmark": "Unmark key point",
|
|
680
|
+
"timeline.noMarks": "No marked key points"
|
|
419
681
|
};
|
|
420
682
|
//#endregion
|
|
421
683
|
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -481,9 +743,10 @@ window.__ModuleLoader__.load({
|
|
|
481
743
|
/**
|
|
482
744
|
* Persist the section through the host route. Concurrent calls are
|
|
483
745
|
* serialized by a request sequence: an older save that resolves after a
|
|
484
|
-
* newer one
|
|
746
|
+
* newer one answers 'superseded' instead of 'ok', so the caller can never
|
|
747
|
+
* mistake a stale write for success and adopt its outdated section.
|
|
485
748
|
* @param section - the complete next section.
|
|
486
|
-
* @returns whether the
|
|
749
|
+
* @returns whether the write committed, was displaced by a newer save, or failed.
|
|
487
750
|
*/
|
|
488
751
|
async save(section) {
|
|
489
752
|
const seq = ++this.saveSeq;
|
|
@@ -495,16 +758,16 @@ window.__ModuleLoader__.load({
|
|
|
495
758
|
body: JSON.stringify(section)
|
|
496
759
|
});
|
|
497
760
|
const body = await response.json();
|
|
498
|
-
if (!response.ok || !body.ok || body.value === void 0) return
|
|
499
|
-
if (seq !== this.saveSeq) return
|
|
761
|
+
if (!response.ok || !body.ok || body.value === void 0) return "failed";
|
|
762
|
+
if (seq !== this.saveSeq) return "superseded";
|
|
500
763
|
this.snapshot = {
|
|
501
764
|
status: "ready",
|
|
502
765
|
value: body.value
|
|
503
766
|
};
|
|
504
767
|
this.notify();
|
|
505
|
-
return
|
|
768
|
+
return "ok";
|
|
506
769
|
} catch {
|
|
507
|
-
return
|
|
770
|
+
return "failed";
|
|
508
771
|
}
|
|
509
772
|
};
|
|
510
773
|
const promise = run();
|
|
@@ -536,7 +799,7 @@ window.__ModuleLoader__.load({
|
|
|
536
799
|
const settingsClient = new SettingsClient();
|
|
537
800
|
//#endregion
|
|
538
801
|
//#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
|
|
539
|
-
const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.
|
|
802
|
+
const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.42)) brightness(var(--bg-glass-brightness,1));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#fff;opacity:.8;text-shadow:0 1px 2px #00000059;white-space:nowrap;font-size:11px;line-height:1}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}.-ozqdq_switch{background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:11px;flex:none;width:40px;height:22px;padding:0;transition:background-color .15s;position:relative}.-ozqdq_switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}.-ozqdq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_switchKnob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px;box-shadow:0 1px 3px #00000047}.-ozqdq_switch[aria-checked=true] .-ozqdq_switchKnob{transform:translate(18px)}@media (prefers-reduced-motion:reduce){.-ozqdq_btn,.-ozqdq_switch,.-ozqdq_switchKnob{transition:none}}";
|
|
540
803
|
const tagId = "deepseek-harness-background/SettingsRow.module.css";
|
|
541
804
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
542
805
|
const tag = document.createElement("style");
|
|
@@ -546,39 +809,41 @@ window.__ModuleLoader__.load({
|
|
|
546
809
|
document.head.appendChild(tag);
|
|
547
810
|
}
|
|
548
811
|
var SettingsRow_module_css_default = {
|
|
549
|
-
"
|
|
550
|
-
"
|
|
551
|
-
"
|
|
552
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
812
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
813
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
814
|
+
"segBtn": "-ozqdq_segBtn",
|
|
553
815
|
"header": "-ozqdq_header",
|
|
816
|
+
"clearRow": "-ozqdq_clearRow",
|
|
817
|
+
"switchKnob": "-ozqdq_switchKnob",
|
|
818
|
+
"switch": "-ozqdq_switch",
|
|
819
|
+
"segGroup": "-ozqdq_segGroup",
|
|
820
|
+
"segActive": "-ozqdq_segActive",
|
|
821
|
+
"segRow": "-ozqdq_segRow",
|
|
822
|
+
"btn": "-ozqdq_btn",
|
|
554
823
|
"previewEmpty": "-ozqdq_previewEmpty",
|
|
555
|
-
"
|
|
824
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
825
|
+
"row": "-ozqdq_row",
|
|
826
|
+
"titleRow": "-ozqdq_titleRow",
|
|
556
827
|
"btnDanger": "-ozqdq_btnDanger",
|
|
828
|
+
"desc": "-ozqdq_desc",
|
|
557
829
|
"sliderRow": "-ozqdq_sliderRow",
|
|
558
|
-
"segGroup": "-ozqdq_segGroup",
|
|
559
|
-
"clearRow": "-ozqdq_clearRow",
|
|
560
|
-
"slider": "-ozqdq_slider",
|
|
561
|
-
"sliderValue": "-ozqdq_sliderValue",
|
|
562
|
-
"hint": "-ozqdq_hint",
|
|
563
830
|
"segLabel": "-ozqdq_segLabel",
|
|
564
|
-
"segBtn": "-ozqdq_segBtn",
|
|
565
|
-
"sliderLabel": "-ozqdq_sliderLabel",
|
|
566
|
-
"segRow": "-ozqdq_segRow",
|
|
567
|
-
"desc": "-ozqdq_desc",
|
|
568
|
-
"urlRow": "-ozqdq_urlRow",
|
|
569
|
-
"previewImg": "-ozqdq_previewImg",
|
|
570
|
-
"segActive": "-ozqdq_segActive",
|
|
571
|
-
"error": "-ozqdq_error",
|
|
572
831
|
"title": "-ozqdq_title",
|
|
832
|
+
"urlRow": "-ozqdq_urlRow",
|
|
833
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
834
|
+
"sliderLabel": "-ozqdq_sliderLabel",
|
|
835
|
+
"hint": "-ozqdq_hint",
|
|
836
|
+
"slider": "-ozqdq_slider",
|
|
573
837
|
"urlInput": "-ozqdq_urlInput",
|
|
574
|
-
"btn": "-ozqdq_btn",
|
|
575
838
|
"btnPrimary": "-ozqdq_btnPrimary",
|
|
576
|
-
"
|
|
577
|
-
"
|
|
578
|
-
"
|
|
579
|
-
"
|
|
839
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
840
|
+
"error": "-ozqdq_error",
|
|
841
|
+
"previewImg": "-ozqdq_previewImg",
|
|
842
|
+
"sectionLabel": "-ozqdq_sectionLabel",
|
|
580
843
|
"section": "-ozqdq_section",
|
|
581
|
-
"
|
|
844
|
+
"preview": "-ozqdq_preview",
|
|
845
|
+
"sliderValue": "-ozqdq_sliderValue",
|
|
846
|
+
"previewGlassText": "-ozqdq_previewGlassText"
|
|
582
847
|
};
|
|
583
848
|
//#endregion
|
|
584
849
|
//#region src/client/SettingsRow.tsx
|
|
@@ -610,7 +875,8 @@ window.__ModuleLoader__.load({
|
|
|
610
875
|
panelOpacity: DEFAULT_PANEL_OPACITY,
|
|
611
876
|
blur: 16,
|
|
612
877
|
wallpaperBlur: 0,
|
|
613
|
-
fit: DEFAULT_FIT
|
|
878
|
+
fit: DEFAULT_FIT,
|
|
879
|
+
timeline: true
|
|
614
880
|
};
|
|
615
881
|
/** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
|
|
616
882
|
* the two blur sliders feel smooth near small values. Bounds come from the
|
|
@@ -624,7 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
624
890
|
},
|
|
625
891
|
scrim: {
|
|
626
892
|
min: 0,
|
|
627
|
-
max:
|
|
893
|
+
max: 1,
|
|
628
894
|
step: .05,
|
|
629
895
|
unit: "percent"
|
|
630
896
|
},
|
|
@@ -658,7 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
658
924
|
}
|
|
659
925
|
/** Field-wise equality for distinguishing our own saves from external changes. */
|
|
660
926
|
function sameSettings(a, b) {
|
|
661
|
-
return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit;
|
|
927
|
+
return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit && a.timeline === b.timeline;
|
|
662
928
|
}
|
|
663
929
|
/**
|
|
664
930
|
* A stepped range control. While dragging it re-renders only itself: the value
|
|
@@ -783,23 +1049,29 @@ window.__ModuleLoader__.load({
|
|
|
783
1049
|
/** Persist one section through the host route and adopt it on success. */
|
|
784
1050
|
const saveNow = (0, react.useCallback)(async (next) => {
|
|
785
1051
|
dirtyRef.current = true;
|
|
786
|
-
const
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
lastSavedRef.current = next;
|
|
791
|
-
draftRef.current = next;
|
|
792
|
-
setDraft(next);
|
|
793
|
-
if (!next.uploadId) setUrlText(next.url);
|
|
1052
|
+
const result = await settingsClient.save(next);
|
|
1053
|
+
if (result === "failed") {
|
|
1054
|
+
setError(t("background.saveFailed"));
|
|
1055
|
+
return;
|
|
794
1056
|
}
|
|
1057
|
+
if (result === "superseded") return;
|
|
1058
|
+
dirtyRef.current = false;
|
|
1059
|
+
setError("");
|
|
1060
|
+
lastSavedRef.current = next;
|
|
1061
|
+
draftRef.current = next;
|
|
1062
|
+
setDraft(next);
|
|
1063
|
+
if (!next.uploadId) setUrlText(next.url);
|
|
795
1064
|
}, [t]);
|
|
1065
|
+
const saveNowRef = (0, react.useRef)(saveNow);
|
|
1066
|
+
saveNowRef.current = saveNow;
|
|
796
1067
|
/** Debounced commit: one POST per drag gesture, from the latest draft. */
|
|
797
1068
|
const onCommit = (0, react.useCallback)(() => {
|
|
798
1069
|
clearTimeout(commitTimer.current);
|
|
799
1070
|
commitTimer.current = window.setTimeout(() => {
|
|
800
|
-
|
|
1071
|
+
commitTimer.current = void 0;
|
|
1072
|
+
saveNowRef.current(draftRef.current);
|
|
801
1073
|
}, 120);
|
|
802
|
-
}, [
|
|
1074
|
+
}, []);
|
|
803
1075
|
(0, react.useEffect)(() => {
|
|
804
1076
|
if (snapshot.status !== "ready" || snapshot.value === void 0) return;
|
|
805
1077
|
const saved = lastSavedRef.current;
|
|
@@ -812,8 +1084,13 @@ window.__ModuleLoader__.load({
|
|
|
812
1084
|
}
|
|
813
1085
|
}, [snapshot]);
|
|
814
1086
|
(0, react.useEffect)(() => () => {
|
|
815
|
-
clearTimeout(commitTimer.current);
|
|
816
1087
|
clearTimeout(editingTimer.current);
|
|
1088
|
+
editingRef.current = false;
|
|
1089
|
+
if (commitTimer.current !== void 0) {
|
|
1090
|
+
clearTimeout(commitTimer.current);
|
|
1091
|
+
commitTimer.current = void 0;
|
|
1092
|
+
saveNowRef.current(draftRef.current);
|
|
1093
|
+
}
|
|
817
1094
|
if (dirtyRef.current) settingsClient.flush();
|
|
818
1095
|
}, []);
|
|
819
1096
|
const handleFile = async (file) => {
|
|
@@ -845,7 +1122,7 @@ window.__ModuleLoader__.load({
|
|
|
845
1122
|
const handleUrlSubmit = () => {
|
|
846
1123
|
const url = urlText.trim();
|
|
847
1124
|
if (!/^https?:\/\//i.test(url)) {
|
|
848
|
-
setError(t("background.
|
|
1125
|
+
setError(t("background.invalidUrl"));
|
|
849
1126
|
return;
|
|
850
1127
|
}
|
|
851
1128
|
markEditing();
|
|
@@ -885,6 +1162,16 @@ window.__ModuleLoader__.load({
|
|
|
885
1162
|
paintLive(next);
|
|
886
1163
|
saveNow(next);
|
|
887
1164
|
};
|
|
1165
|
+
const handleTimelineToggle = () => {
|
|
1166
|
+
markEditing();
|
|
1167
|
+
const next = {
|
|
1168
|
+
...draftRef.current,
|
|
1169
|
+
timeline: !draftRef.current.timeline
|
|
1170
|
+
};
|
|
1171
|
+
draftRef.current = next;
|
|
1172
|
+
setDraft(next);
|
|
1173
|
+
saveNow(next);
|
|
1174
|
+
};
|
|
888
1175
|
const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
|
|
889
1176
|
const hasImage = sourceUrl !== "";
|
|
890
1177
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -916,7 +1203,8 @@ window.__ModuleLoader__.load({
|
|
|
916
1203
|
className: SettingsRow_module_css_default.previewImg,
|
|
917
1204
|
src: sourceUrl,
|
|
918
1205
|
alt: "",
|
|
919
|
-
draggable: false
|
|
1206
|
+
draggable: false,
|
|
1207
|
+
referrerPolicy: "no-referrer"
|
|
920
1208
|
}),
|
|
921
1209
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
|
|
922
1210
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -960,7 +1248,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1248
|
}),
|
|
961
1249
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
962
1250
|
className: SettingsRow_module_css_default.hint,
|
|
963
|
-
children: t("background.
|
|
1251
|
+
children: t("background.uploadMediaTypes")
|
|
964
1252
|
}),
|
|
965
1253
|
draft.uploadId && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
966
1254
|
className: SettingsRow_module_css_default.enabledBadge,
|
|
@@ -1046,6 +1334,29 @@ window.__ModuleLoader__.load({
|
|
|
1046
1334
|
children: t(mode === "cover" ? "background.cover" : "background.contain")
|
|
1047
1335
|
}, mode))
|
|
1048
1336
|
})]
|
|
1337
|
+
}),
|
|
1338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1339
|
+
className: SettingsRow_module_css_default.segRow,
|
|
1340
|
+
children: [
|
|
1341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1342
|
+
className: SettingsRow_module_css_default.segLabel,
|
|
1343
|
+
children: t("background.timeline")
|
|
1344
|
+
}),
|
|
1345
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1346
|
+
type: "button",
|
|
1347
|
+
role: "switch",
|
|
1348
|
+
"aria-checked": draft.timeline,
|
|
1349
|
+
"aria-label": t("background.timeline"),
|
|
1350
|
+
title: t("background.timelineHint"),
|
|
1351
|
+
className: SettingsRow_module_css_default.switch,
|
|
1352
|
+
onClick: handleTimelineToggle,
|
|
1353
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsRow_module_css_default.switchKnob })
|
|
1354
|
+
}),
|
|
1355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1356
|
+
className: SettingsRow_module_css_default.hint,
|
|
1357
|
+
children: t("background.timelineHint")
|
|
1358
|
+
})
|
|
1359
|
+
]
|
|
1049
1360
|
})
|
|
1050
1361
|
]
|
|
1051
1362
|
}),
|
|
@@ -1067,17 +1378,862 @@ window.__ModuleLoader__.load({
|
|
|
1067
1378
|
});
|
|
1068
1379
|
}
|
|
1069
1380
|
//#endregion
|
|
1381
|
+
//#region src/client/timeline-css.ts
|
|
1382
|
+
/**
|
|
1383
|
+
* Timeline CSS — the conversation scroll-navigation rail (DeepSeek web
|
|
1384
|
+
* ScrollNav structure, rebuilt on this plugin's frosted-glass system).
|
|
1385
|
+
*
|
|
1386
|
+
* Structure and metrics mirror the official chat.deepseek.com stylesheet
|
|
1387
|
+
* (module map extracted from the shipped bundle; v0.1.4 of the reference port
|
|
1388
|
+
* dsh-chat-timeline widens the panel to 260px and adds bookmarks):
|
|
1389
|
+
* nav 34px-wide rail, fixed right edge, vertically centered; collapsed
|
|
1390
|
+
* blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
|
|
1391
|
+
* whose scroll area holds 30px rows (tick indicator + fading-in title);
|
|
1392
|
+
* 32px gradient fade veils top/bottom shown while that side clips.
|
|
1393
|
+
*
|
|
1394
|
+
* Glass integration: the official chat.deepseek.com paints (light
|
|
1395
|
+
* rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
|
|
1396
|
+
* 5px collapsed / 16px expanded blur) remain as the NO-GLASS fallback.
|
|
1397
|
+
* While the plugin's glass system is on (`body[data-dsh-bg-glass]`), the
|
|
1398
|
+
* rail joins the SAME unified recipe as the composer card and bubbles:
|
|
1399
|
+
* fill = the painter's `--dsw-specific-input-major` token, blur/saturate/
|
|
1400
|
+
* brightness = the shared `--bg-glass-*` variables, i.e. the glass-blur
|
|
1401
|
+
* slider. Class prefix `dsbt-` deliberately avoids every
|
|
1402
|
+
* `[class*="_xxx"]` anchor of the plugin's glass sheet (underscores only).
|
|
1403
|
+
*/
|
|
1404
|
+
/** Unique id stamped on the injected style element (dedup key). */
|
|
1405
|
+
const TIMELINE_CSS_TAG = "deepseek-harness-background/timeline";
|
|
1406
|
+
/** Inject the timeline stylesheet once. Idempotent (dedup key on the tag). */
|
|
1407
|
+
function injectTimelineCss() {
|
|
1408
|
+
if (typeof document === "undefined") return;
|
|
1409
|
+
if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
|
|
1410
|
+
const tag = document.createElement("style");
|
|
1411
|
+
tag.dataset.plugin = "deepseek-harness-background";
|
|
1412
|
+
tag.dataset.pluginCss = TIMELINE_CSS_TAG;
|
|
1413
|
+
tag.textContent = TIMELINE_CSS;
|
|
1414
|
+
document.head.appendChild(tag);
|
|
1415
|
+
}
|
|
1416
|
+
const TIMELINE_CSS = `
|
|
1417
|
+
.dsbt-nav {
|
|
1418
|
+
user-select: none; -webkit-user-select: none;
|
|
1419
|
+
z-index: 90; align-items: center; width: 34px;
|
|
1420
|
+
height: var(--dsbt-h, 300px);
|
|
1421
|
+
transition: right .2s ease, opacity .2s ease;
|
|
1422
|
+
display: flex; position: fixed; top: 50%; right: 16px;
|
|
1423
|
+
transform: translateY(-50%); pointer-events: auto;
|
|
1424
|
+
}
|
|
1425
|
+
@media (max-width: 767px) { .dsbt-nav { display: none; } }
|
|
1426
|
+
|
|
1427
|
+
/* Collapsed capsule — frosted strip behind the tick marks. Official paint:
|
|
1428
|
+
light rgba(255,255,255,.8), dark rgba(21,21,23,.6), 5px blur. */
|
|
1429
|
+
.dsbt-bg {
|
|
1430
|
+
background-color: rgba(255, 255, 255, .8);
|
|
1431
|
+
-webkit-backdrop-filter: blur(5px);
|
|
1432
|
+
backdrop-filter: blur(5px);
|
|
1433
|
+
box-shadow:
|
|
1434
|
+
inset 0 1px 0 rgba(255, 255, 255, .28),
|
|
1435
|
+
inset 0 0 0 .5px rgba(255, 255, 255, .1),
|
|
1436
|
+
0 6px 18px rgba(0, 0, 0, .07);
|
|
1437
|
+
border-radius: 16px; width: 34px;
|
|
1438
|
+
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1439
|
+
position: absolute; top: 50%; right: 0; transform: translateY(-50%);
|
|
1440
|
+
transition: opacity .2s ease; pointer-events: none;
|
|
1441
|
+
}
|
|
1442
|
+
body[data-ds-dark-theme] .dsbt-bg {
|
|
1443
|
+
background-color: rgba(21, 21, 23, .6);
|
|
1444
|
+
box-shadow:
|
|
1445
|
+
inset 0 1px 0 rgba(255, 255, 255, .09),
|
|
1446
|
+
inset 0 0 0 .5px rgba(255, 255, 255, .05),
|
|
1447
|
+
0 6px 18px rgba(0, 0, 0, .3);
|
|
1448
|
+
}
|
|
1449
|
+
.dsbt-bg.dsbt-bg-hide { opacity: 0; }
|
|
1450
|
+
|
|
1451
|
+
/* Expanding panel — exactly the capsule's box (identical height in both
|
|
1452
|
+
states), width animates 34px -> measured fit (max 260px). Official
|
|
1453
|
+
hover-state paint: white/black frosted glass at ~94% alpha, 16px blur. */
|
|
1454
|
+
.dsbt-wrap {
|
|
1455
|
+
width: 34px;
|
|
1456
|
+
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1457
|
+
border: 1px solid transparent; border-radius: 16px;
|
|
1458
|
+
flex-direction: column; align-items: stretch;
|
|
1459
|
+
transition: width .22s cubic-bezier(.4, 0, .2, 1), background-color .2s ease,
|
|
1460
|
+
box-shadow .2s ease, border-color .2s ease;
|
|
1461
|
+
display: flex; position: absolute; top: 50%; right: 0;
|
|
1462
|
+
transform: translateY(-50%);
|
|
1463
|
+
overflow: hidden; box-sizing: border-box; background: transparent;
|
|
1464
|
+
pointer-events: none;
|
|
1465
|
+
}
|
|
1466
|
+
.dsbt-wrap.dsbt-show {
|
|
1467
|
+
pointer-events: auto;
|
|
1468
|
+
width: min(var(--dsbt-w, 260px), 260px);
|
|
1469
|
+
background-color: rgba(255, 255, 255, .94);
|
|
1470
|
+
-webkit-backdrop-filter: blur(16px);
|
|
1471
|
+
backdrop-filter: blur(16px);
|
|
1472
|
+
border-color: rgba(0, 0, 0, .06);
|
|
1473
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, .08), 0 2px 8px rgba(0, 0, 0, .04);
|
|
1474
|
+
}
|
|
1475
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show {
|
|
1476
|
+
background-color: rgba(28, 28, 32, .95);
|
|
1477
|
+
border-color: rgba(255, 255, 255, .08);
|
|
1478
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .25);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/* Content fade veils — the official ::before/::after pair rebuilt as
|
|
1482
|
+
elements: a bar of the panel's OWN fill color masked to nothing over
|
|
1483
|
+
32px (solid -> mask == gradient-to-transparent, but works with the
|
|
1484
|
+
translucent fill). Shown only while that side actually clips. */
|
|
1485
|
+
.dsbt-fade {
|
|
1486
|
+
z-index: 2; pointer-events: none; opacity: 0;
|
|
1487
|
+
background-color: rgba(255, 255, 255, .94);
|
|
1488
|
+
-webkit-mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1489
|
+
mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1490
|
+
width: 100%; height: 32px; transition: opacity .2s ease;
|
|
1491
|
+
position: absolute; left: 0; top: 0;
|
|
1492
|
+
}
|
|
1493
|
+
body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
|
|
1494
|
+
.dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
|
|
1495
|
+
.dsbt-fade.dsbt-fade-on { opacity: 1; }
|
|
1496
|
+
|
|
1497
|
+
/* Filter bar — "marked only" toggle pinned above the scroll area. */
|
|
1498
|
+
.dsbt-filterbar {
|
|
1499
|
+
padding: 8px 12px 4px; display: flex; justify-content: flex-end;
|
|
1500
|
+
align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
|
|
1501
|
+
box-sizing: border-box; flex: none; transition: opacity .2s ease;
|
|
1502
|
+
}
|
|
1503
|
+
.dsbt-wrap:not(.dsbt-show) .dsbt-filterbar { opacity: 0; }
|
|
1504
|
+
body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
|
|
1505
|
+
.dsbt-filterbtn {
|
|
1506
|
+
font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
|
|
1507
|
+
border: 1px solid rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .03);
|
|
1508
|
+
color: rgba(0, 0, 0, .65); cursor: pointer; transition: all .15s ease;
|
|
1509
|
+
display: inline-flex; align-items: center; gap: 3px; font-family: inherit;
|
|
1510
|
+
}
|
|
1511
|
+
.dsbt-filterbtn:hover { background: rgba(0, 0, 0, .07); color: rgba(0, 0, 0, .9); }
|
|
1512
|
+
.dsbt-filterbtn.dsbt-filteron {
|
|
1513
|
+
background: rgba(245, 158, 11, .12); border-color: #f59e0b;
|
|
1514
|
+
color: #b45309; font-weight: 600;
|
|
1515
|
+
}
|
|
1516
|
+
body[data-ds-dark-theme] .dsbt-filterbtn {
|
|
1517
|
+
border-color: rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .05);
|
|
1518
|
+
color: rgba(255, 255, 255, .7);
|
|
1519
|
+
}
|
|
1520
|
+
body[data-ds-dark-theme] .dsbt-filterbtn:hover { background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .95); }
|
|
1521
|
+
body[data-ds-dark-theme] .dsbt-filterbtn.dsbt-filteron {
|
|
1522
|
+
background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/* Scroll area — rows hug the right edge; scrolls only when overfull. */
|
|
1526
|
+
.dsbt-page {
|
|
1527
|
+
padding: 15px 0 15px 24px; box-sizing: border-box;
|
|
1528
|
+
overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
|
|
1529
|
+
flex-direction: column; align-items: flex-end; display: flex;
|
|
1530
|
+
position: relative; width: 100%; overflow: hidden;
|
|
1531
|
+
}
|
|
1532
|
+
.dsbt-wrap.dsbt-show .dsbt-page {
|
|
1533
|
+
overflow-y: auto; overflow-x: hidden;
|
|
1534
|
+
scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
|
|
1535
|
+
}
|
|
1536
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-page { scrollbar-color: rgba(255, 255, 255, .25) transparent; }
|
|
1537
|
+
.dsbt-page::-webkit-scrollbar { width: 4px; }
|
|
1538
|
+
.dsbt-page::-webkit-scrollbar-track { background: transparent; }
|
|
1539
|
+
.dsbt-page::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, .15); border-radius: 4px; }
|
|
1540
|
+
body[data-ds-dark-theme] .dsbt-page::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .25); }
|
|
1541
|
+
|
|
1542
|
+
/* Empty state (filtered list with no marks). */
|
|
1543
|
+
.dsbt-empty {
|
|
1544
|
+
font-size: 12px; line-height: 18px; padding: 16px 12px;
|
|
1545
|
+
color: rgba(0, 0, 0, .45); text-align: center; width: 100%; box-sizing: border-box;
|
|
1546
|
+
}
|
|
1547
|
+
body[data-ds-dark-theme] .dsbt-empty { color: rgba(255, 255, 255, .45); }
|
|
1548
|
+
|
|
1549
|
+
/* One message row: 30px, star bookmark + tick indicator at the right,
|
|
1550
|
+
title fading in. */
|
|
1551
|
+
.dsbt-item {
|
|
1552
|
+
cursor: pointer; height: 30px; min-height: 30px; flex-shrink: 0;
|
|
1553
|
+
justify-content: flex-end; align-items: center;
|
|
1554
|
+
width: calc(100% - 4px); margin-right: 4px; line-height: 20px;
|
|
1555
|
+
display: flex; background: none; border: none; font: inherit;
|
|
1556
|
+
text-align: right; box-sizing: border-box; padding: 0;
|
|
1557
|
+
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
|
|
1558
|
+
transition: color .15s ease;
|
|
1559
|
+
}
|
|
1560
|
+
.dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
|
|
1561
|
+
body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
|
|
1562
|
+
body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
|
|
1563
|
+
.dsbt-title {
|
|
1564
|
+
font-size: 13px; line-height: 20px; flex: 1 1 auto; min-width: 0;
|
|
1565
|
+
text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
|
|
1566
|
+
margin-right: 6px; text-align: right; color: inherit;
|
|
1567
|
+
opacity: 0; transition: opacity .12s ease, color .15s ease;
|
|
1568
|
+
}
|
|
1569
|
+
.dsbt-show .dsbt-title { opacity: 1; }
|
|
1570
|
+
|
|
1571
|
+
/* Star bookmark — appears on row hover while the panel is expanded; gold
|
|
1572
|
+
when marked. Keyboard-operable (span[role=button]). */
|
|
1573
|
+
.dsbt-star {
|
|
1574
|
+
font-size: 13px; line-height: 16px; width: 18px; height: 18px;
|
|
1575
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1576
|
+
opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
|
|
1577
|
+
flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
|
|
1578
|
+
-webkit-user-select: none;
|
|
1579
|
+
}
|
|
1580
|
+
.dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
|
|
1581
|
+
.dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
|
|
1582
|
+
.dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
|
|
1583
|
+
body[data-ds-dark-theme] .dsbt-star { color: rgba(255, 255, 255, .25); }
|
|
1584
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star:hover { color: #fde047; }
|
|
1585
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #fbbf24; }
|
|
1586
|
+
|
|
1587
|
+
.dsbt-ind { flex-shrink: 0; justify-content: center; align-items: center; width: 16px; height: 20px; display: flex; }
|
|
1588
|
+
.dsbt-line {
|
|
1589
|
+
background-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, .16));
|
|
1590
|
+
border-radius: 4px; flex-shrink: 0; width: 8px; height: 2px;
|
|
1591
|
+
transition: background-color .2s ease, transform .2s ease, width .2s ease, height .2s ease;
|
|
1592
|
+
}
|
|
1593
|
+
.dsbt-item:hover .dsbt-line { background-color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .9)); }
|
|
1594
|
+
.dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1595
|
+
.dsbt-item.dsbt-active .dsbt-title { color: var(--dsw-alias-state-business-primary, #4d6bfe); font-weight: 500; }
|
|
1596
|
+
.dsbt-item.dsbt-active .dsbt-line {
|
|
1597
|
+
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1598
|
+
transform-origin: 50%; transform: scale(1.5);
|
|
1599
|
+
}
|
|
1600
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1601
|
+
body[data-ds-dark-theme] .dsbt-line { background-color: rgba(255, 255, 255, .2); }
|
|
1602
|
+
body[data-ds-dark-theme] .dsbt-item:hover .dsbt-line { background-color: rgba(255, 255, 255, .9); }
|
|
1603
|
+
|
|
1604
|
+
/* Marked (key-point) rows: a wider golden tick in both themes; the active
|
|
1605
|
+
state keeps its brand-blue scale so reading position always wins. */
|
|
1606
|
+
.dsbt-item.dsbt-marked .dsbt-line { background-color: #d97706; width: 10px; height: 2.5px; }
|
|
1607
|
+
.dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #b45309; }
|
|
1608
|
+
.dsbt-item.dsbt-marked.dsbt-active .dsbt-line {
|
|
1609
|
+
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1610
|
+
transform-origin: 50%; transform: scale(1.5);
|
|
1611
|
+
}
|
|
1612
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-marked .dsbt-line { background-color: #fbbf24; width: 10px; height: 2.5px; }
|
|
1613
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #fde047; }
|
|
1614
|
+
|
|
1615
|
+
/* ---- Unified glass integration -----------------------------------------
|
|
1616
|
+
Glass system on (wallpaper active + panelOpacity < 1): the rail joins
|
|
1617
|
+
the composer/bubble recipe — the same translucent fill token and the
|
|
1618
|
+
same blur/saturate/brightness chain (the glass-blur slider drives it,
|
|
1619
|
+
no fixed 5/16px). Placed after the dark-theme rules so equal-specificity
|
|
1620
|
+
overrides win in both schemes. */
|
|
1621
|
+
body[data-dsh-bg-glass] .dsbt-bg,
|
|
1622
|
+
body[data-dsh-bg-glass] .dsbt-wrap.dsbt-show {
|
|
1623
|
+
background-color: var(--dsw-specific-input-major);
|
|
1624
|
+
-webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
1625
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
1626
|
+
}
|
|
1627
|
+
body[data-dsh-bg-glass] .dsbt-fade {
|
|
1628
|
+
background-color: var(--dsw-specific-input-major);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1632
|
+
.dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
|
|
1633
|
+
}
|
|
1634
|
+
`;
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/client/timeline.tsx
|
|
1637
|
+
/**
|
|
1638
|
+
* Conversation timeline rail — DeepSeek web ScrollNav rebuilt on this
|
|
1639
|
+
* plugin's frosted-glass system (styles: ./timeline-css.ts).
|
|
1640
|
+
*
|
|
1641
|
+
* Collapsed: a fixed 34px-wide frosted capsule at the right edge, vertically
|
|
1642
|
+
* centered, one tick per user message (active tick scaled + brand blue,
|
|
1643
|
+
* key-point bookmarks golden). Hovering expands the SAME box into a frosted
|
|
1644
|
+
* panel listing every user question (title fades in; >8 messages widen to
|
|
1645
|
+
* 260px, otherwise the width fits the longest title). Both states share one
|
|
1646
|
+
* computed outer height, so expanding never jumps, and 32px fade veils mask
|
|
1647
|
+
* the clipped edge on each side while that side actually scrolls.
|
|
1648
|
+
*
|
|
1649
|
+
* Enhancements ported from dsh-chat-timeline v0.1.4 (MIT):
|
|
1650
|
+
* - Key-point bookmarks: star a question in the expanded panel, persisted per
|
|
1651
|
+
* session in localStorage; marked rows show a golden tick in the capsule
|
|
1652
|
+
* and a "marked only" filter narrows the list.
|
|
1653
|
+
* - Jump stabilization: the reading-position tracker freezes while a smooth
|
|
1654
|
+
* jump animates (cleared 150ms after the last scroll event, plus an 800ms
|
|
1655
|
+
* fallback), so the panel cannot jitter mid-jump.
|
|
1656
|
+
* - Rewind integration: messages withdrawn by a rewind command node are
|
|
1657
|
+
* filtered out of the rail.
|
|
1658
|
+
*
|
|
1659
|
+
* Data source (client-side only): loaded chat nodes -> background loadOlder
|
|
1660
|
+
* loop until complete. Clicking a row loads older history on demand, scrolls
|
|
1661
|
+
* the conversation to the target row, and keeps the rail's own active row
|
|
1662
|
+
* visible.
|
|
1663
|
+
*
|
|
1664
|
+
* The rail reads the shared settings transport: it renders unless
|
|
1665
|
+
* `timeline: false`. Its glass paints are self-contained official values
|
|
1666
|
+
* (see timeline-css.ts), independent of the plugin's token overrides.
|
|
1667
|
+
*/
|
|
1668
|
+
if (typeof document !== "undefined") injectTimelineCss();
|
|
1669
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1670
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
1671
|
+
function userTextOf(content) {
|
|
1672
|
+
if (!Array.isArray(content)) return "";
|
|
1673
|
+
let out = "";
|
|
1674
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
|
|
1675
|
+
const text = block.text;
|
|
1676
|
+
if (typeof text === "string") out += text;
|
|
1677
|
+
}
|
|
1678
|
+
return out.trim().slice(0, 80);
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Stable bookmark key for one entry: the durable message key, falling back to
|
|
1682
|
+
* the anchor seq — so marks survive history reloads. Entries with neither are
|
|
1683
|
+
* not markable (empty key).
|
|
1684
|
+
* @param m - the entry (or any shaped object).
|
|
1685
|
+
*/
|
|
1686
|
+
function markKeyOf(m) {
|
|
1687
|
+
if (m === null || typeof m !== "object") return "";
|
|
1688
|
+
const rec = m;
|
|
1689
|
+
if (typeof rec.key === "string" && rec.key !== "") return `key:${rec.key}`;
|
|
1690
|
+
return typeof rec.seq === "number" ? `seq:${rec.seq}` : "";
|
|
1691
|
+
}
|
|
1692
|
+
/** localStorage key prefix for the per-session marked-key lists. */
|
|
1693
|
+
const MARKS_STORAGE_PREFIX = "dsbt_marks_";
|
|
1694
|
+
/**
|
|
1695
|
+
* Read the marked keys for one session from localStorage.
|
|
1696
|
+
* @param sessionId - the session the marks belong to.
|
|
1697
|
+
*/
|
|
1698
|
+
function readMarks(sessionId) {
|
|
1699
|
+
if (typeof window === "undefined" || !sessionId) return [];
|
|
1700
|
+
try {
|
|
1701
|
+
const raw = window.localStorage.getItem(MARKS_STORAGE_PREFIX + sessionId);
|
|
1702
|
+
if (!raw) return [];
|
|
1703
|
+
const parsed = JSON.parse(raw);
|
|
1704
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
1705
|
+
} catch {
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Persist the marked keys for one session (removes the key when empty).
|
|
1711
|
+
* @param sessionId - the session the marks belong to.
|
|
1712
|
+
* @param marks - the marked-key list.
|
|
1713
|
+
*/
|
|
1714
|
+
function writeMarks(sessionId, marks) {
|
|
1715
|
+
if (typeof window === "undefined" || !sessionId) return;
|
|
1716
|
+
try {
|
|
1717
|
+
if (!Array.isArray(marks) || marks.length === 0) window.localStorage.removeItem(MARKS_STORAGE_PREFIX + sessionId);
|
|
1718
|
+
else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
|
|
1719
|
+
} catch {}
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Extract the rewind target seq from a rewind outcome text.
|
|
1723
|
+
* @param text - the command outcome text.
|
|
1724
|
+
*/
|
|
1725
|
+
function rewindTargetOfOutcome(text) {
|
|
1726
|
+
if (typeof text !== "string") return void 0;
|
|
1727
|
+
const digits = (/rewound.*?(?:target\s+)?(\d+)/i.exec(text) ?? /#(\d+)/.exec(text))?.[1];
|
|
1728
|
+
if (digits === void 0) return void 0;
|
|
1729
|
+
const n = Number.parseInt(digits, 10);
|
|
1730
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Is this rewind command a preview/dry-run (marks the target hidden without
|
|
1734
|
+
* an outcome)?
|
|
1735
|
+
* @param command - the command payload.
|
|
1736
|
+
*/
|
|
1737
|
+
function isRewindPreviewCommand(command) {
|
|
1738
|
+
if (command.args === null || typeof command.args !== "object") return false;
|
|
1739
|
+
const args = command.args;
|
|
1740
|
+
return args.preview === true || args.dryRun === true;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Collect the seq numbers a rewind hid: preview commands hide their target
|
|
1744
|
+
* directly; successful commands hide their own row plus every node anchored
|
|
1745
|
+
* inside the rewound span [target, sourceEventSeq].
|
|
1746
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
1747
|
+
*/
|
|
1748
|
+
function hiddenSeqsOfChat(chat) {
|
|
1749
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
1750
|
+
if (chat === null || typeof chat !== "object") return hidden;
|
|
1751
|
+
const nodes = chat.nodes;
|
|
1752
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
|
|
1753
|
+
const all = [...nodes.values()];
|
|
1754
|
+
const spans = [];
|
|
1755
|
+
for (const node of all) {
|
|
1756
|
+
if (node === null || typeof node !== "object") continue;
|
|
1757
|
+
if (node.kind !== "command") continue;
|
|
1758
|
+
const command = node.data;
|
|
1759
|
+
if (command === null || typeof command !== "object") continue;
|
|
1760
|
+
if (command.name !== "rewind") continue;
|
|
1761
|
+
if (isRewindPreviewCommand(command)) {
|
|
1762
|
+
if (typeof command.seq === "number") hidden.add(command.seq);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const outcome = command.outcome;
|
|
1766
|
+
if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
|
|
1767
|
+
const marker = outcome.sourceEventSeq;
|
|
1768
|
+
if (typeof marker !== "number") continue;
|
|
1769
|
+
if (typeof command.seq === "number") hidden.add(command.seq);
|
|
1770
|
+
const target = rewindTargetOfOutcome(outcome.text);
|
|
1771
|
+
if (target !== void 0) spans.push({
|
|
1772
|
+
start: target,
|
|
1773
|
+
end: marker
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
if (spans.length === 0) return hidden;
|
|
1777
|
+
for (const node of all) {
|
|
1778
|
+
if (node === null || typeof node !== "object") continue;
|
|
1779
|
+
const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
|
|
1780
|
+
if (anchor === void 0) continue;
|
|
1781
|
+
if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
|
|
1782
|
+
}
|
|
1783
|
+
return hidden;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Enumerate user messages from the loaded chat-node snapshot (the client-side
|
|
1787
|
+
* collector; sorted by anchor sequence, rewind-hidden entries dropped).
|
|
1788
|
+
* @param snapshot - a session snapshot (loosely typed on purpose).
|
|
1789
|
+
*/
|
|
1790
|
+
function collectMessages(snapshot) {
|
|
1791
|
+
const chat = snapshot?.chat;
|
|
1792
|
+
const nodes = chat?.nodes;
|
|
1793
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return [];
|
|
1794
|
+
const hidden = hiddenSeqsOfChat(chat);
|
|
1795
|
+
const out = [];
|
|
1796
|
+
for (const node of nodes.values()) {
|
|
1797
|
+
if (node === null || typeof node !== "object") continue;
|
|
1798
|
+
if (node.kind !== "user") continue;
|
|
1799
|
+
const data = node.data;
|
|
1800
|
+
if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
1801
|
+
const key = typeof node.key === "string" ? node.key : void 0;
|
|
1802
|
+
if (key === void 0) continue;
|
|
1803
|
+
const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
|
|
1804
|
+
if (hidden.has(seq)) continue;
|
|
1805
|
+
out.push({
|
|
1806
|
+
seq,
|
|
1807
|
+
time: data.time,
|
|
1808
|
+
text: userTextOf(data.content),
|
|
1809
|
+
key
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
1813
|
+
return out;
|
|
1814
|
+
}
|
|
1815
|
+
/** Resolve the DOM anchor key of one entry (direct key form only). */
|
|
1816
|
+
function resolveAnchorKey(m) {
|
|
1817
|
+
return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Outer rail height for a message count: rows + breathing room, clamped so
|
|
1821
|
+
* both states always share the exact same height (never jumps on expand).
|
|
1822
|
+
* @param count - number of user messages.
|
|
1823
|
+
*/
|
|
1824
|
+
function railHeightFor(count) {
|
|
1825
|
+
return Math.max(140, Math.min(300, count * 30 + 38));
|
|
1826
|
+
}
|
|
1827
|
+
/** Longest-title width estimate bounds. */
|
|
1828
|
+
const RAIL_MIN_WIDTH = 96;
|
|
1829
|
+
const RAIL_MAX_WIDTH = 260;
|
|
1830
|
+
/**
|
|
1831
|
+
* Measure the expanded-panel width for the given titles (canvas text metrics;
|
|
1832
|
+
* falls back to the max width when measurement is unavailable). Accounts for
|
|
1833
|
+
* the star column so a marked row never clips its tick.
|
|
1834
|
+
* @param texts - the message previews.
|
|
1835
|
+
*/
|
|
1836
|
+
function railWidthFor(texts) {
|
|
1837
|
+
if (texts.length > 8) return RAIL_MAX_WIDTH;
|
|
1838
|
+
let widest = 0;
|
|
1839
|
+
if (typeof document !== "undefined") {
|
|
1840
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
1841
|
+
if (ctx) {
|
|
1842
|
+
ctx.font = "13px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
|
|
1843
|
+
for (const t of texts) {
|
|
1844
|
+
const w = ctx.measureText(t === "" ? "…" : t).width;
|
|
1845
|
+
if (w > widest) widest = w;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
|
|
1850
|
+
}
|
|
1851
|
+
/** Ensure the target node is loaded, then scroll its row into view. */
|
|
1852
|
+
async function jumpToMessage(sessionsService, sessionId, key) {
|
|
1853
|
+
const session = sessionsService.binding(sessionId)?.session;
|
|
1854
|
+
if (session === void 0) return false;
|
|
1855
|
+
let guard = 0;
|
|
1856
|
+
while (guard++ < 120) {
|
|
1857
|
+
const snapshot = session.getSnapshot();
|
|
1858
|
+
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
1859
|
+
if (snapshot?.hasMore !== true) return false;
|
|
1860
|
+
if (snapshot.loadingOlder === true) {
|
|
1861
|
+
await delay(50);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
await session.loadOlder();
|
|
1865
|
+
}
|
|
1866
|
+
const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
|
|
1867
|
+
if (row === null) return false;
|
|
1868
|
+
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1869
|
+
row.scrollIntoView({
|
|
1870
|
+
behavior: reducedMotion ? "auto" : "smooth",
|
|
1871
|
+
block: "center"
|
|
1872
|
+
});
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Scroll only the rail itself so the active row stays visible (min distance).
|
|
1877
|
+
* offsetTop math instead of getBoundingClientRect: stable while the panel's
|
|
1878
|
+
* own smooth scroll animates (no feedback loop with live rects).
|
|
1879
|
+
*/
|
|
1880
|
+
function scrollTimelineItemIntoView(page, item) {
|
|
1881
|
+
const pageTop = page.scrollTop;
|
|
1882
|
+
const pageBottom = pageTop + page.clientHeight;
|
|
1883
|
+
const itemTop = item.offsetTop;
|
|
1884
|
+
const itemBottom = itemTop + item.offsetHeight;
|
|
1885
|
+
if (itemTop < pageTop) page.scrollTop = Math.max(0, itemTop - 10);
|
|
1886
|
+
else if (itemBottom > pageBottom) page.scrollTop = itemBottom - page.clientHeight + 10;
|
|
1887
|
+
}
|
|
1888
|
+
/** Is another timeline rail already mounted (e.g. dsh-chat-timeline)? */
|
|
1889
|
+
function otherRailPresent() {
|
|
1890
|
+
return document.querySelector(".dsct_nav") !== null;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* The timeline rail component. Portaled to body; registered into the
|
|
1894
|
+
* conversation input dock purely to bind per-session lifecycle.
|
|
1895
|
+
*/
|
|
1896
|
+
function TimelineRail(props) {
|
|
1897
|
+
const { sessionId, sessionsService, t } = props;
|
|
1898
|
+
const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
|
|
1899
|
+
const timelineUnknown = settings.status === "loading";
|
|
1900
|
+
const timelineDisabled = settings.value?.timeline === false;
|
|
1901
|
+
const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
|
|
1902
|
+
const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
|
|
1903
|
+
const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
|
|
1904
|
+
const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
|
|
1905
|
+
const messages = react.default.useMemo(() => collectMessages(nodeSnapshot), [nodeSnapshot]);
|
|
1906
|
+
const [loadedAll, setLoadedAll] = react.default.useState(false);
|
|
1907
|
+
const [activeIndex, setActiveIndex] = react.default.useState(-1);
|
|
1908
|
+
const [show, setShow] = react.default.useState(false);
|
|
1909
|
+
const [rightOffset, setRightOffset] = react.default.useState(16);
|
|
1910
|
+
const [railWidth, setRailWidth] = react.default.useState(RAIL_MAX_WIDTH);
|
|
1911
|
+
const [fades, setFades] = react.default.useState({
|
|
1912
|
+
top: false,
|
|
1913
|
+
bottom: false
|
|
1914
|
+
});
|
|
1915
|
+
const [hidden, setHidden] = react.default.useState(() => otherRailPresent());
|
|
1916
|
+
const [marks, setMarks] = react.default.useState(() => readMarks(sessionId));
|
|
1917
|
+
const [marksOnly, setMarksOnly] = react.default.useState(false);
|
|
1918
|
+
const [isNarrow, setIsNarrow] = react.default.useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches);
|
|
1919
|
+
const pageRef = react.default.useRef(null);
|
|
1920
|
+
const activeItemRef = react.default.useRef(null);
|
|
1921
|
+
const jumpPendingRef = react.default.useRef(false);
|
|
1922
|
+
const jumpFallbackRef = react.default.useRef(void 0);
|
|
1923
|
+
react.default.useEffect(() => {
|
|
1924
|
+
setMarks(readMarks(sessionId));
|
|
1925
|
+
setMarksOnly(false);
|
|
1926
|
+
}, [sessionId]);
|
|
1927
|
+
const markedSet = react.default.useMemo(() => new Set(marks), [marks]);
|
|
1928
|
+
const marksRef = react.default.useRef(marks);
|
|
1929
|
+
marksRef.current = marks;
|
|
1930
|
+
const toggleMark = react.default.useCallback((m) => {
|
|
1931
|
+
const k = markKeyOf(m);
|
|
1932
|
+
if (k === "") return;
|
|
1933
|
+
const prev = marksRef.current;
|
|
1934
|
+
const next = prev.includes(k) ? prev.filter((x) => x !== k) : [...prev, k];
|
|
1935
|
+
marksRef.current = next;
|
|
1936
|
+
setMarks(next);
|
|
1937
|
+
writeMarks(sessionId, next);
|
|
1938
|
+
}, [sessionId]);
|
|
1939
|
+
react.default.useEffect(() => {
|
|
1940
|
+
if (typeof window.matchMedia !== "function") return;
|
|
1941
|
+
const mql = window.matchMedia("(max-width: 767px)");
|
|
1942
|
+
const onChange = (e) => setIsNarrow(e.matches);
|
|
1943
|
+
mql.addEventListener("change", onChange);
|
|
1944
|
+
return () => mql.removeEventListener("change", onChange);
|
|
1945
|
+
}, []);
|
|
1946
|
+
react.default.useEffect(() => {
|
|
1947
|
+
if (session === void 0 || isNarrow || sessionId === void 0 || timelineUnknown || timelineDisabled || hidden) return;
|
|
1948
|
+
setLoadedAll(false);
|
|
1949
|
+
let cancelled = false;
|
|
1950
|
+
const run = async () => {
|
|
1951
|
+
let guard = 0;
|
|
1952
|
+
while (!cancelled && guard++ < 120) {
|
|
1953
|
+
const snap = session.getSnapshot();
|
|
1954
|
+
if (snap?.hasMore !== true) break;
|
|
1955
|
+
if (snap.loadingOlder === true) {
|
|
1956
|
+
await delay(50);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
await session.loadOlder();
|
|
1960
|
+
}
|
|
1961
|
+
if (!cancelled) setLoadedAll(true);
|
|
1962
|
+
};
|
|
1963
|
+
run().catch(() => {
|
|
1964
|
+
if (!cancelled) setLoadedAll(true);
|
|
1965
|
+
});
|
|
1966
|
+
return () => {
|
|
1967
|
+
cancelled = true;
|
|
1968
|
+
};
|
|
1969
|
+
}, [
|
|
1970
|
+
sessionId,
|
|
1971
|
+
isNarrow,
|
|
1972
|
+
session,
|
|
1973
|
+
timelineUnknown,
|
|
1974
|
+
timelineDisabled,
|
|
1975
|
+
hidden
|
|
1976
|
+
]);
|
|
1977
|
+
react.default.useEffect(() => {
|
|
1978
|
+
if (!loadedAll && messages.length > 0) return;
|
|
1979
|
+
setRailWidth(railWidthFor(messages.map((m) => m.text)));
|
|
1980
|
+
}, [loadedAll, messages]);
|
|
1981
|
+
react.default.useEffect(() => {
|
|
1982
|
+
if (messages.length === 0) return;
|
|
1983
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
1984
|
+
messages.forEach((m, i) => {
|
|
1985
|
+
const key = resolveAnchorKey(m);
|
|
1986
|
+
if (key !== void 0) indexByKey.set(key, i);
|
|
1987
|
+
});
|
|
1988
|
+
const updateActive = () => {
|
|
1989
|
+
if (jumpPendingRef.current) return;
|
|
1990
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
1991
|
+
if (sp === null) return;
|
|
1992
|
+
const rect = sp.getBoundingClientRect();
|
|
1993
|
+
if (rect.height === 0) return;
|
|
1994
|
+
const line = rect.top + rect.height * .4;
|
|
1995
|
+
const rows = sp.querySelectorAll("[data-chat-anchor-key]");
|
|
1996
|
+
let best = -1;
|
|
1997
|
+
let bestDist = Infinity;
|
|
1998
|
+
for (const row of rows) {
|
|
1999
|
+
const key = row.getAttribute("data-chat-anchor-key");
|
|
2000
|
+
if (key === null) continue;
|
|
2001
|
+
const idx = indexByKey.get(key) ?? -1;
|
|
2002
|
+
if (idx === -1) continue;
|
|
2003
|
+
const r = row.getBoundingClientRect();
|
|
2004
|
+
const dist = Math.abs(r.top + r.height / 2 - line);
|
|
2005
|
+
if (dist < bestDist) {
|
|
2006
|
+
bestDist = dist;
|
|
2007
|
+
best = idx;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
setActiveIndex(best);
|
|
2011
|
+
};
|
|
2012
|
+
updateActive();
|
|
2013
|
+
const el = document.querySelector("[data-conversation-scroll]");
|
|
2014
|
+
let timer;
|
|
2015
|
+
let settleTimer;
|
|
2016
|
+
const onScroll = () => {
|
|
2017
|
+
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2018
|
+
settleTimer = window.setTimeout(() => {
|
|
2019
|
+
settleTimer = void 0;
|
|
2020
|
+
if (jumpPendingRef.current) {
|
|
2021
|
+
jumpPendingRef.current = false;
|
|
2022
|
+
if (jumpFallbackRef.current !== void 0) {
|
|
2023
|
+
clearTimeout(jumpFallbackRef.current);
|
|
2024
|
+
jumpFallbackRef.current = void 0;
|
|
2025
|
+
}
|
|
2026
|
+
updateActive();
|
|
2027
|
+
}
|
|
2028
|
+
}, 150);
|
|
2029
|
+
if (timer !== void 0) return;
|
|
2030
|
+
timer = window.setTimeout(() => {
|
|
2031
|
+
timer = void 0;
|
|
2032
|
+
updateActive();
|
|
2033
|
+
}, 60);
|
|
2034
|
+
};
|
|
2035
|
+
el?.addEventListener("scroll", onScroll, { passive: true });
|
|
2036
|
+
const interval = window.setInterval(updateActive, 2e3);
|
|
2037
|
+
return () => {
|
|
2038
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2039
|
+
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2040
|
+
if (jumpFallbackRef.current !== void 0) {
|
|
2041
|
+
clearTimeout(jumpFallbackRef.current);
|
|
2042
|
+
jumpFallbackRef.current = void 0;
|
|
2043
|
+
}
|
|
2044
|
+
el?.removeEventListener("scroll", onScroll);
|
|
2045
|
+
clearInterval(interval);
|
|
2046
|
+
};
|
|
2047
|
+
}, [messages]);
|
|
2048
|
+
const syncFades = react.default.useCallback(() => {
|
|
2049
|
+
const page = pageRef.current;
|
|
2050
|
+
if (page === null) {
|
|
2051
|
+
setFades({
|
|
2052
|
+
top: false,
|
|
2053
|
+
bottom: false
|
|
2054
|
+
});
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
const canTop = page.scrollTop > 2;
|
|
2058
|
+
const canBot = page.scrollTop + page.clientHeight < page.scrollHeight - 2;
|
|
2059
|
+
setFades((prev) => prev.top === canTop && prev.bottom === canBot ? prev : {
|
|
2060
|
+
top: canTop,
|
|
2061
|
+
bottom: canBot
|
|
2062
|
+
});
|
|
2063
|
+
}, []);
|
|
2064
|
+
react.default.useEffect(syncFades, [
|
|
2065
|
+
syncFades,
|
|
2066
|
+
show,
|
|
2067
|
+
messages.length,
|
|
2068
|
+
loadedAll,
|
|
2069
|
+
marksOnly
|
|
2070
|
+
]);
|
|
2071
|
+
react.default.useLayoutEffect(() => {
|
|
2072
|
+
if (!show) return;
|
|
2073
|
+
const page = pageRef.current;
|
|
2074
|
+
const item = activeItemRef.current;
|
|
2075
|
+
if (page === null || item === null) return;
|
|
2076
|
+
scrollTimelineItemIntoView(page, item);
|
|
2077
|
+
syncFades();
|
|
2078
|
+
}, [
|
|
2079
|
+
activeIndex,
|
|
2080
|
+
messages.length,
|
|
2081
|
+
marksOnly,
|
|
2082
|
+
show,
|
|
2083
|
+
syncFades
|
|
2084
|
+
]);
|
|
2085
|
+
react.default.useEffect(() => {
|
|
2086
|
+
let raf = 0;
|
|
2087
|
+
const measure = () => {
|
|
2088
|
+
raf = 0;
|
|
2089
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2090
|
+
if (sp === null) return;
|
|
2091
|
+
const rect = sp.getBoundingClientRect();
|
|
2092
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
2093
|
+
const next = Math.max(8, Math.round(window.innerWidth - rect.right + 12));
|
|
2094
|
+
setRightOffset((prev) => Math.abs(prev - next) > .5 ? next : prev);
|
|
2095
|
+
};
|
|
2096
|
+
const schedule = () => {
|
|
2097
|
+
if (raf === 0) raf = window.requestAnimationFrame(measure);
|
|
2098
|
+
};
|
|
2099
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2100
|
+
const ro = typeof ResizeObserver === "function" && sp !== null ? new ResizeObserver(schedule) : null;
|
|
2101
|
+
if (ro !== null && sp !== null) ro.observe(sp);
|
|
2102
|
+
window.addEventListener("resize", schedule);
|
|
2103
|
+
const guard = window.setInterval(() => setHidden(otherRailPresent()), 2e3);
|
|
2104
|
+
measure();
|
|
2105
|
+
return () => {
|
|
2106
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
2107
|
+
ro?.disconnect();
|
|
2108
|
+
window.removeEventListener("resize", schedule);
|
|
2109
|
+
clearInterval(guard);
|
|
2110
|
+
};
|
|
2111
|
+
}, []);
|
|
2112
|
+
if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length < 2) return null;
|
|
2113
|
+
const height = railHeightFor(messages.length);
|
|
2114
|
+
const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
|
|
2115
|
+
const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
|
|
2116
|
+
return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2117
|
+
className: "dsbt-nav",
|
|
2118
|
+
style: {
|
|
2119
|
+
right: rightOffset,
|
|
2120
|
+
["--dsbt-h"]: `${height}px`,
|
|
2121
|
+
["--dsbt-w"]: `${railWidth}px`
|
|
2122
|
+
},
|
|
2123
|
+
role: "navigation",
|
|
2124
|
+
"aria-label": t("timeline.railLabel"),
|
|
2125
|
+
onMouseEnter: () => setShow(true),
|
|
2126
|
+
onMouseLeave: () => setShow(false),
|
|
2127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: show ? "dsbt-bg dsbt-bg-hide" : "dsbt-bg" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2128
|
+
className: show ? "dsbt-wrap dsbt-show" : "dsbt-wrap",
|
|
2129
|
+
children: [
|
|
2130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2131
|
+
className: "dsbt-filterbar",
|
|
2132
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2133
|
+
type: "button",
|
|
2134
|
+
className: marksOnly ? "dsbt-filterbtn dsbt-filteron" : "dsbt-filterbtn",
|
|
2135
|
+
"aria-pressed": marksOnly,
|
|
2136
|
+
onClick: () => setMarksOnly((v) => !v),
|
|
2137
|
+
children: [t("timeline.marksOnly"), marks.length > 0 ? ` (${marks.length})` : ""]
|
|
2138
|
+
})
|
|
2139
|
+
}),
|
|
2140
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2141
|
+
className: fades.top ? "dsbt-fade dsbt-fade-on" : "dsbt-fade",
|
|
2142
|
+
"aria-hidden": true
|
|
2143
|
+
}),
|
|
2144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2145
|
+
className: fades.bottom ? "dsbt-fade dsbt-fade-bot dsbt-fade-on" : "dsbt-fade dsbt-fade-bot",
|
|
2146
|
+
"aria-hidden": true
|
|
2147
|
+
}),
|
|
2148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2149
|
+
className: "dsbt-page",
|
|
2150
|
+
ref: pageRef,
|
|
2151
|
+
onScroll: syncFades,
|
|
2152
|
+
children: displayMessages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2153
|
+
className: "dsbt-empty",
|
|
2154
|
+
children: t("timeline.noMarks")
|
|
2155
|
+
}) : displayMessages.map((m) => {
|
|
2156
|
+
const key = resolveAnchorKey(m);
|
|
2157
|
+
const mk = markKeyOf(m);
|
|
2158
|
+
const marked = markedSet.has(mk);
|
|
2159
|
+
const isActive = activeKey !== "" && mk === activeKey;
|
|
2160
|
+
const cls = `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`;
|
|
2161
|
+
const label = `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`;
|
|
2162
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2163
|
+
type: "button",
|
|
2164
|
+
ref: isActive ? activeItemRef : void 0,
|
|
2165
|
+
className: cls,
|
|
2166
|
+
title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
|
|
2167
|
+
"aria-label": label,
|
|
2168
|
+
"aria-current": isActive ? "location" : void 0,
|
|
2169
|
+
onClick: () => {
|
|
2170
|
+
if (key === void 0) return;
|
|
2171
|
+
jumpPendingRef.current = true;
|
|
2172
|
+
if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
|
|
2173
|
+
jumpFallbackRef.current = window.setTimeout(() => {
|
|
2174
|
+
jumpFallbackRef.current = void 0;
|
|
2175
|
+
jumpPendingRef.current = false;
|
|
2176
|
+
}, 800);
|
|
2177
|
+
jumpToMessage(sessionsService, sessionId, key).catch(() => {});
|
|
2178
|
+
},
|
|
2179
|
+
children: [
|
|
2180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2181
|
+
className: "dsbt-title",
|
|
2182
|
+
children: m.text === "" ? t("timeline.noText") : m.text
|
|
2183
|
+
}),
|
|
2184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2185
|
+
className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
|
|
2186
|
+
role: "button",
|
|
2187
|
+
tabIndex: 0,
|
|
2188
|
+
"aria-pressed": marked,
|
|
2189
|
+
"aria-label": marked ? t("timeline.unmark") : t("timeline.mark"),
|
|
2190
|
+
onMouseDown: (e) => {
|
|
2191
|
+
e.stopPropagation();
|
|
2192
|
+
e.preventDefault();
|
|
2193
|
+
},
|
|
2194
|
+
onClick: (e) => {
|
|
2195
|
+
e.stopPropagation();
|
|
2196
|
+
toggleMark(m);
|
|
2197
|
+
},
|
|
2198
|
+
onKeyDown: (e) => {
|
|
2199
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2200
|
+
e.preventDefault();
|
|
2201
|
+
e.stopPropagation();
|
|
2202
|
+
toggleMark(m);
|
|
2203
|
+
}
|
|
2204
|
+
},
|
|
2205
|
+
children: "★"
|
|
2206
|
+
}),
|
|
2207
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2208
|
+
className: "dsbt-ind",
|
|
2209
|
+
"aria-hidden": true,
|
|
2210
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsbt-line" })
|
|
2211
|
+
})
|
|
2212
|
+
]
|
|
2213
|
+
}, m.seq);
|
|
2214
|
+
})
|
|
2215
|
+
})
|
|
2216
|
+
]
|
|
2217
|
+
})]
|
|
2218
|
+
}), document.body);
|
|
2219
|
+
}
|
|
2220
|
+
//#endregion
|
|
1070
2221
|
//#region src/client/index.ts
|
|
1071
2222
|
/**
|
|
1072
|
-
* Service injections this entry declares:
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
2223
|
+
* Service injections this entry declares: `slots` + `locale` for the settings
|
|
2224
|
+
* surface and the timeline slot seat, and `sessions` so the timeline rail can
|
|
2225
|
+
* enumerate user messages and jump between them. The settings surface itself
|
|
2226
|
+
* talks to the plugin's own host route over plain same-origin fetch.
|
|
1075
2227
|
*/
|
|
1076
|
-
const inject = [
|
|
2228
|
+
const inject = [
|
|
2229
|
+
"slots",
|
|
2230
|
+
"locale",
|
|
2231
|
+
"sessions"
|
|
2232
|
+
];
|
|
1077
2233
|
/**
|
|
1078
|
-
* Client plugin body: paint the persisted background live
|
|
1079
|
-
* General-settings row.
|
|
1080
|
-
* @param ctx - client cordis context (slots/locale injected).
|
|
2234
|
+
* Client plugin body: paint the persisted background live, register the
|
|
2235
|
+
* General-settings row, and mount the conversation timeline rail.
|
|
2236
|
+
* @param ctx - client cordis context (slots/locale/sessions injected).
|
|
1081
2237
|
*/
|
|
1082
2238
|
function apply(ctx) {
|
|
1083
2239
|
const paint = () => {
|
|
@@ -1101,6 +2257,13 @@ window.__ModuleLoader__.load({
|
|
|
1101
2257
|
order: 600,
|
|
1102
2258
|
locale: "ui-background"
|
|
1103
2259
|
}, BackgroundSettingsRow));
|
|
2260
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
2261
|
+
name: "conversation.input.dock",
|
|
2262
|
+
id: "deepseek-harness-background.timeline",
|
|
2263
|
+
order: 45,
|
|
2264
|
+
locale: "ui-background",
|
|
2265
|
+
inject: () => ({ sessionsService: ctx.sessions })
|
|
2266
|
+
}, TimelineRail));
|
|
1104
2267
|
ctx.effect(() => () => {
|
|
1105
2268
|
unsubscribe();
|
|
1106
2269
|
backgroundPainter.dispose();
|