deepseek-harness-background 0.2.1 → 0.3.2
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 +9 -2
- package/README.zh.md +9 -2
- package/lib/client.js +1423 -77
- package/lib/client.js.map +1 -1
- package/lib/index.js +119 -6
- package/package.json +8 -8
package/lib/client.js
CHANGED
|
@@ -4,28 +4,85 @@ 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 load-earlier history button, the composer
|
|
65
|
+
* dock family (agent task strips: TodoPanel / GoalBar / QueueDock) and its
|
|
66
|
+
* takeover panels (approval, question, plan review), the subagent lineage
|
|
67
|
+
* popover, the home hero "preview" badge, the sidebar build badge, and the
|
|
68
|
+
* timeline rail (timeline-css.ts). Every glassed surface shares ONE recipe:
|
|
69
|
+
* fill from the painter's --dsw-specific-input-major token (same alpha
|
|
70
|
+
* curve + theme dimming), the full shared blur/saturate/brightness chain
|
|
71
|
+
* driven by the glass-blur slider, and the shared sheen vars — so no
|
|
72
|
+
* surface reads heavier than the composer card. Dialogs, the settings UI,
|
|
73
|
+
* menus, tooltips, toasts and every hover/accent fill keep the OFFICIAL
|
|
74
|
+
* opaque paints — they are reading surfaces and must stay legible.
|
|
75
|
+
*
|
|
76
|
+
* Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
|
|
77
|
+
* `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
|
|
78
|
+
* SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions:
|
|
79
|
+
* `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
|
|
80
|
+
* `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
|
|
81
|
+
* it), `_newSession` excludes `Label` (the button's inner label span) and
|
|
82
|
+
* `_toBottom` excludes `Slot` (the zero-height sticky wrapper) — both share
|
|
83
|
+
* the substring with the chrome control but must keep their own paint, and
|
|
84
|
+
* the subagent popover is `role="tree"` + `_menu` so the generic menu
|
|
85
|
+
* surface stays untouched.
|
|
29
86
|
*/
|
|
30
87
|
const BACKGROUND_CSS = `
|
|
31
88
|
.dsh-bg-layer {
|
|
@@ -58,22 +115,165 @@ window.__ModuleLoader__.load({
|
|
|
58
115
|
--dsw-specific-sidebar-fill: transparent;
|
|
59
116
|
}
|
|
60
117
|
|
|
61
|
-
/*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
/* ---- Whitelisted frosted-glass sheet ---------------------------------
|
|
119
|
+
Fills come from the --dsw-* tokens the painter overrides on body ONLY for
|
|
120
|
+
this list's own surfaces (input/bubble/code/tip); this block adds the
|
|
121
|
+
shared wet-glass sheen + blur from --bg-glass-blur. Exposure is calibrated
|
|
122
|
+
per scheme (the painter writes --bg-glass-brightness / --bg-glass-sheen):
|
|
123
|
+
light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
|
|
124
|
+
GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
|
|
125
|
+
slider returns these surfaces to the exact official paints. */
|
|
126
|
+
body[data-dsh-bg-glass] [data-composer-card],
|
|
127
|
+
body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
|
|
128
|
+
body[data-dsh-bg-glass] .md-code-block,
|
|
129
|
+
body[data-dsh-bg-glass] [data-terminal],
|
|
130
|
+
body[data-dsh-bg-glass] [data-diff],
|
|
131
|
+
body[data-dsh-bg-glass] [data-read],
|
|
132
|
+
body[data-dsh-bg-glass] [data-search],
|
|
133
|
+
body[data-dsh-bg-glass] [data-web],
|
|
134
|
+
body[data-dsh-bg-glass] [class*="_ioCard"],
|
|
135
|
+
body[data-dsh-bg-glass] [class*="_instructionsCard"],
|
|
136
|
+
body[data-dsh-bg-glass] [class*="_markdown"] :not(pre) > code {
|
|
137
|
+
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));
|
|
138
|
+
-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);
|
|
139
|
+
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
140
|
box-shadow:
|
|
73
|
-
inset 0 1px 0 rgba(255, 255, 255,
|
|
141
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
74
142
|
inset 0 -1px 0 rgba(255, 255, 255, 0.08),
|
|
75
143
|
inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
|
|
76
|
-
0 12px 40px rgba(0, 0, 0,
|
|
144
|
+
0 12px 40px rgba(0, 0, 0, 0.12);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* The sticky code-block header officially occludes scrolled code with
|
|
148
|
+
bg-base (transparent under a wallpaper): refill it with the banner token
|
|
149
|
+
(itself translucent glass) so code stays hidden beneath the sticky bar. */
|
|
150
|
+
body[data-dsh-bg] .md-code-block [class*="_bannerWrap"] {
|
|
151
|
+
background-color: var(--dsw-alias-markdown-code-block-banner);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* The sidebar build badge paints with the label-ink token — a TEXT token we
|
|
155
|
+
must not override globally — so it gets a theme-aware translucent ink. */
|
|
156
|
+
body[data-dsh-bg] [class*="_buildRevision"] {
|
|
157
|
+
background-color: rgba(15, 17, 21, 0.62);
|
|
158
|
+
}
|
|
159
|
+
body[data-ds-dark-theme][data-dsh-bg] [class*="_buildRevision"] {
|
|
160
|
+
background-color: rgba(249, 250, 251, 0.62);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* The empty-state hero glow asset would wash out the wallpaper behind the
|
|
164
|
+
welcome message — dim it so the art stays visible. */
|
|
165
|
+
body[data-dsh-bg] [class*="_heroGlow"] {
|
|
166
|
+
opacity: 0.2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ---- Whitelisted chrome buttons --------------------------------------
|
|
170
|
+
These three buttons' official fills ride button-* tokens shared with
|
|
171
|
+
unrelated surfaces, so instead of overriding the tokens they get explicit
|
|
172
|
+
paints — but the SAME glass recipe as the composer card and bubbles:
|
|
173
|
+
fill = the painter's --dsw-specific-input-major token (the identical
|
|
174
|
+
translucent white + theme curve), filter = the sheet's exact
|
|
175
|
+
blur/saturate/brightness chain (blur follows the glass-blur slider, no
|
|
176
|
+
caps), sheen = the shared sheen vars. Hover = the same white glass at the
|
|
177
|
+
boosted alpha var. Gated on data-dsh-bg-glass (off when the panel is
|
|
178
|
+
fully opaque). */
|
|
179
|
+
body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]),
|
|
180
|
+
body[data-dsh-bg-glass] [data-composer-card] [class*="_add"],
|
|
181
|
+
body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]) {
|
|
182
|
+
background-color: var(--dsw-specific-input-major);
|
|
183
|
+
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));
|
|
184
|
+
-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);
|
|
185
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
186
|
+
box-shadow:
|
|
187
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
188
|
+
inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
|
|
189
|
+
0 4px 14px rgba(0, 0, 0, 0.08);
|
|
190
|
+
}
|
|
191
|
+
body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]):hover,
|
|
192
|
+
body[data-dsh-bg-glass] [data-composer-card] [class*="_add"]:hover:not(:disabled),
|
|
193
|
+
body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]):hover {
|
|
194
|
+
background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
|
|
195
|
+
}
|
|
196
|
+
/* Collapsed rail renders the new-session control as a bare icon — keep the
|
|
197
|
+
official transparent paint (and its official translucent hover). */
|
|
198
|
+
body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"],
|
|
199
|
+
body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"]:hover {
|
|
200
|
+
background-color: transparent;
|
|
201
|
+
background-image: none;
|
|
202
|
+
box-shadow: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ---- Subagent lineage popover (title-bar expanded list) --------------
|
|
206
|
+
Its official fill rides the menu token shared with every dropdown; glass
|
|
207
|
+
THIS popover explicitly via its role="tree" + _menu combination so real
|
|
208
|
+
menus stay officially opaque and legible. Same unified recipe: the
|
|
209
|
+
composer fill token + the full shared filter chain (the official
|
|
210
|
+
elevation shadow stays). */
|
|
211
|
+
body[data-dsh-bg-glass] [role="tree"][class*="_menu"] {
|
|
212
|
+
background-color: var(--dsw-specific-input-major);
|
|
213
|
+
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));
|
|
214
|
+
-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);
|
|
215
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ---- Home hero "preview" badge (top-right superscript pill) ----------
|
|
219
|
+
Officially the state-business-tertiary pastel; re-emit the same hue as
|
|
220
|
+
glass but with the UNIFIED alpha curve (color-mix against the painter's
|
|
221
|
+
--bg-glass-alpha) and the shared blur chain, so it sits on the wallpaper
|
|
222
|
+
with exactly the glass strength of the composer card. */
|
|
223
|
+
body[data-dsh-bg-glass] [class*="_previewBadge"] {
|
|
224
|
+
background-color: color-mix(in srgb, rgb(228 237 253) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
|
|
225
|
+
-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);
|
|
226
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
227
|
+
}
|
|
228
|
+
body[data-ds-dark-theme][data-dsh-bg-glass] [class*="_previewBadge"] {
|
|
229
|
+
background-color: color-mix(in srgb, rgb(52 65 91) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ---- Composer dock family: task strips + takeover panels --------------
|
|
233
|
+
The agent task strips (TodoPanel, GoalBar, QueueDock — one dock column
|
|
234
|
+
above the composer) and the composer takeovers (approval, question,
|
|
235
|
+
plan review) paint with tokens the painter turns translucent
|
|
236
|
+
(--dsw-specific-tip / --dsw-specific-input-major) but their own styles
|
|
237
|
+
never blur anything: translucent-without-blur reads as bare
|
|
238
|
+
transparency, not frosted glass. Give each the shared sheen + filter
|
|
239
|
+
chain; their fills stay token-driven so the panel-opacity slider keeps
|
|
240
|
+
driving them. Anchors are the components' own stable data attributes
|
|
241
|
+
plus a structural child, so no generic class suffix is swept in. */
|
|
242
|
+
body[data-dsh-bg-glass] [data-testid="todo-panel"],
|
|
243
|
+
body[data-dsh-bg-glass] [data-goal-bar] > div,
|
|
244
|
+
body[data-dsh-bg-glass] [data-queue-dock] > div,
|
|
245
|
+
body[data-dsh-bg-glass] [data-approval-key] > div,
|
|
246
|
+
body[data-dsh-bg-glass] [data-question-key] > section,
|
|
247
|
+
body[data-dsh-bg-glass] [data-plan-review-key] > section {
|
|
248
|
+
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));
|
|
249
|
+
-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);
|
|
250
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ---- "Load earlier" history button (top of long conversations) --------
|
|
254
|
+
Its official fill is an opaque solid button token that is deliberately
|
|
255
|
+
NOT overridden, so unlike the tip family it needs an explicit glass
|
|
256
|
+
fill too — same recipe as the three chrome buttons above. */
|
|
257
|
+
body[data-dsh-bg-glass] [class*="_older"] button {
|
|
258
|
+
background-color: var(--dsw-specific-input-major);
|
|
259
|
+
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));
|
|
260
|
+
-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);
|
|
261
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
262
|
+
box-shadow:
|
|
263
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
|
264
|
+
inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
|
|
265
|
+
0 4px 14px rgba(0, 0, 0, 0.08);
|
|
266
|
+
}
|
|
267
|
+
body[data-dsh-bg-glass] [class*="_older"] button:hover:not(:disabled) {
|
|
268
|
+
background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Sidebar build badge: translucent ink already, but it sat directly on
|
|
272
|
+
the wallpaper without frost. Join the shared blur chain under the same
|
|
273
|
+
gate (its theme-aware ink fills from the active block above). */
|
|
274
|
+
body[data-dsh-bg-glass] [class*="_buildRevision"] {
|
|
275
|
+
-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);
|
|
276
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
77
277
|
}
|
|
78
278
|
`;
|
|
79
279
|
/**
|
|
@@ -96,6 +296,12 @@ window.__ModuleLoader__.load({
|
|
|
96
296
|
const DEFAULT_SCRIM = .25;
|
|
97
297
|
/** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
|
|
98
298
|
const DEFAULT_PANEL_OPACITY = .15;
|
|
299
|
+
/**
|
|
300
|
+
* Session-projection key this plugin owns in the client-visible value map —
|
|
301
|
+
* the host fold registers it (src/projection.ts) and the rail reads it
|
|
302
|
+
* (src/client/timeline.tsx); one shared constant so the two can never drift.
|
|
303
|
+
*/
|
|
304
|
+
const TIMELINE_PROJECTION_KEY = "bgTimeline";
|
|
99
305
|
/** Default background rendering mode. */
|
|
100
306
|
const DEFAULT_FIT = "cover";
|
|
101
307
|
/** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
|
|
@@ -117,22 +323,102 @@ window.__ModuleLoader__.load({
|
|
|
117
323
|
const LAYER_CLASS = "dsh-bg-layer";
|
|
118
324
|
const IMAGE_CLASS = "dsh-bg-image";
|
|
119
325
|
const SCRIM_CLASS = "dsh-bg-scrim";
|
|
120
|
-
/**
|
|
121
|
-
|
|
326
|
+
/** Body style properties the painter owns while a wallpaper paints: the app
|
|
327
|
+
* frame and sidebar fills go transparent so every column shares one art.
|
|
328
|
+
* Inline values outrank any stylesheet rule (the body[data-dsh-bg] gate only
|
|
329
|
+
* covers the CSS copy), so a sourceless section must actively restore them. */
|
|
330
|
+
const FRAME_TOKENS = ["--dsw-alias-bg-base", "--dsw-specific-sidebar-fill"];
|
|
331
|
+
/**
|
|
332
|
+
* White-glass tokens — the WHITELISTED surfaces only. Everything that floats
|
|
333
|
+
* as a small card over the wallpaper: the composer input stack, message
|
|
334
|
+
* bubbles, the markdown code surfaces (blocks + banner + inline code), and
|
|
335
|
+
* the agent task strip family (`--dsw-specific-tip`: TodoPanel + GoalBar +
|
|
336
|
+
* QueueDock share one dock column and one fill token). Menus, dialogs, the
|
|
337
|
+
* settings UI, tooltips, toasts, hover fills and every accent keep the
|
|
338
|
+
* OFFICIAL token values — they are reading surfaces and must stay legible.
|
|
339
|
+
* `factor` scales the alpha relative to the input surface.
|
|
340
|
+
*/
|
|
341
|
+
const GLASS_SURFACE_TOKENS = [
|
|
342
|
+
{
|
|
343
|
+
token: "--dsw-specific-input-major",
|
|
344
|
+
factor: 1
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
token: "--dsw-specific-bubble",
|
|
348
|
+
factor: .8
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
token: "--dsw-alias-markdown-code-block",
|
|
352
|
+
factor: 1
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
token: "--dsw-alias-markdown-code-block-banner",
|
|
356
|
+
factor: 1
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
token: "--dsw-alias-markdown-inline-code",
|
|
360
|
+
factor: .9
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
token: "--dsw-specific-tip",
|
|
364
|
+
factor: 1
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
/** Every token the glass owns (save/restore key set). */
|
|
368
|
+
const GLASS_TOKENS = GLASS_SURFACE_TOKENS.map(({ token }) => token);
|
|
122
369
|
/** Fraction of the official white surface alpha kept at the most transparent panel setting. */
|
|
123
370
|
const GLASS_MIN_ALPHA = .05;
|
|
371
|
+
/** Base white-glass alpha for one panelOpacity (the curve every surface shares). */
|
|
372
|
+
function glassAlpha(panelOpacity) {
|
|
373
|
+
const alpha = GLASS_MIN_ALPHA + panelOpacity * .85;
|
|
374
|
+
return Math.max(0, Math.min(.9, alpha));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Saturation boost that rides the blur radius. Gentler than the reference
|
|
378
|
+
* engine (1.1 + blur * 0.02, capped at 1.6): stacking a hot saturate on top of
|
|
379
|
+
* the brightness/sheen layers pushed bright wallpapers into clipping.
|
|
380
|
+
*/
|
|
381
|
+
function glassSaturate(blur) {
|
|
382
|
+
if (blur <= 0) return 1;
|
|
383
|
+
return Math.round(Math.min(1.6, 1.1 + blur * .02) * 1e3) / 1e3;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Exposure calibration per scheme. Light glass neither boosts brightness nor
|
|
387
|
+
* stacks a heavy sheen (mainstream frosted recipes — macOS vibrancy, Windows
|
|
388
|
+
* acrylic, common web glass — blur + saturate + tint without a positive
|
|
389
|
+
* brightness gain); dark glass keeps the reference engine's slight lift, which
|
|
390
|
+
* stops the white-glass fills from turning muddy.
|
|
391
|
+
*/
|
|
392
|
+
function glassExposure(inDark) {
|
|
393
|
+
return inDark ? {
|
|
394
|
+
brightness: "1.04",
|
|
395
|
+
sheen: "0.16",
|
|
396
|
+
sheenMid: "0.05"
|
|
397
|
+
} : {
|
|
398
|
+
brightness: "0.98",
|
|
399
|
+
sheen: "0.07",
|
|
400
|
+
sheenMid: "0.02"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The theme-aware base alpha for one panelOpacity — THE curve every glass
|
|
405
|
+
* surface shares (surface tokens, bare alpha knobs and the settings preview
|
|
406
|
+
* included). Single-sourced here so the live painter and the preview card
|
|
407
|
+
* cannot drift.
|
|
408
|
+
*/
|
|
409
|
+
function glassBaseAlpha(panelOpacity, inDark) {
|
|
410
|
+
return glassAlpha(panelOpacity) * (inDark ? .4 : .8);
|
|
411
|
+
}
|
|
124
412
|
/**
|
|
125
413
|
* The theme-aware glass surface alphas for one panelOpacity: the input-surface
|
|
126
414
|
* token and the bubble token share one curve, dimmed per scheme. Shared by the
|
|
127
415
|
* live painter and the settings-row preview card so the two cannot drift.
|
|
128
416
|
*/
|
|
129
417
|
function glassSurfaceAlphas(panelOpacity, inDark) {
|
|
130
|
-
const alpha =
|
|
131
|
-
const clamped = Math.max(0, Math.min(.9, alpha));
|
|
132
|
-
const factor = inDark ? .4 : .8;
|
|
418
|
+
const alpha = glassBaseAlpha(panelOpacity, inDark);
|
|
133
419
|
return {
|
|
134
|
-
major: `rgba(255, 255, 255, ${
|
|
135
|
-
bubble: `rgba(255, 255, 255, ${(
|
|
420
|
+
major: `rgba(255, 255, 255, ${alpha.toFixed(3)})`,
|
|
421
|
+
bubble: `rgba(255, 255, 255, ${(alpha * .8).toFixed(3)})`
|
|
136
422
|
};
|
|
137
423
|
}
|
|
138
424
|
/**
|
|
@@ -160,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
160
446
|
rememberOnce(prop) {
|
|
161
447
|
if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
|
|
162
448
|
const value = document.body.style.getPropertyValue(prop);
|
|
163
|
-
if (
|
|
449
|
+
if (GLASS_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
|
|
164
450
|
else this.savedVars.set(prop, value);
|
|
165
451
|
}
|
|
166
452
|
/**
|
|
@@ -175,20 +461,21 @@ window.__ModuleLoader__.load({
|
|
|
175
461
|
return;
|
|
176
462
|
}
|
|
177
463
|
injectBackgroundCss();
|
|
178
|
-
this.rememberOnce("--dsw-alias-bg-base");
|
|
179
|
-
this.rememberOnce("--dsw-specific-sidebar-fill");
|
|
180
|
-
document.body.style.setProperty("--dsw-alias-bg-base", "transparent");
|
|
181
|
-
document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
|
|
182
464
|
const url = backgroundImageUrl(settings, uploadBase);
|
|
183
465
|
if (url === "") {
|
|
184
466
|
this.observer?.disconnect();
|
|
185
467
|
this.observer = void 0;
|
|
468
|
+
this.restoreFrameTokens();
|
|
186
469
|
this.applySurfaceGlass(settings, true);
|
|
187
470
|
this.active = false;
|
|
188
471
|
this.removeLayers();
|
|
189
472
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
190
473
|
return;
|
|
191
474
|
}
|
|
475
|
+
for (const prop of FRAME_TOKENS) {
|
|
476
|
+
this.rememberOnce(prop);
|
|
477
|
+
document.body.style.setProperty(prop, "transparent");
|
|
478
|
+
}
|
|
192
479
|
if (!this.layer) {
|
|
193
480
|
this.layer = document.createElement("div");
|
|
194
481
|
this.layer.className = LAYER_CLASS;
|
|
@@ -214,7 +501,7 @@ window.__ModuleLoader__.load({
|
|
|
214
501
|
this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
|
|
215
502
|
this.setVar("--bg-scrim", String(settings.scrim));
|
|
216
503
|
this.setVar("--bg-glass-blur", `${settings.blur}px`);
|
|
217
|
-
this.setVar("--bg-glass-saturate", String(
|
|
504
|
+
this.setVar("--bg-glass-saturate", String(glassSaturate(settings.blur)));
|
|
218
505
|
this.applySurfaceGlass(settings, false);
|
|
219
506
|
if (!this.observer) {
|
|
220
507
|
this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
|
|
@@ -230,6 +517,20 @@ window.__ModuleLoader__.load({
|
|
|
230
517
|
document.body.style.setProperty(name, value);
|
|
231
518
|
}
|
|
232
519
|
/**
|
|
520
|
+
* Return the owned frame-transparency properties to their saved state (or
|
|
521
|
+
* remove them when this session never overrode them). dispose() restores
|
|
522
|
+
* everything wholesale; this targeted variant serves the sourceless path,
|
|
523
|
+
* where the painter keeps running for a later source.
|
|
524
|
+
*/
|
|
525
|
+
restoreFrameTokens() {
|
|
526
|
+
for (const prop of FRAME_TOKENS) {
|
|
527
|
+
this.rememberOnce(prop);
|
|
528
|
+
const original = this.savedVars.get(prop);
|
|
529
|
+
if (original !== void 0 && original !== "") document.body.style.setProperty(prop, original);
|
|
530
|
+
else document.body.style.removeProperty(prop);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
233
534
|
* Update one effect knob without re-running the full apply. This is the hot
|
|
234
535
|
* path for slider drags (many events per second): only the single CSS
|
|
235
536
|
* variable changes, the DOM layers are untouched.
|
|
@@ -247,7 +548,7 @@ window.__ModuleLoader__.load({
|
|
|
247
548
|
break;
|
|
248
549
|
case "blur":
|
|
249
550
|
this.setVar("--bg-glass-blur", `${value}px`);
|
|
250
|
-
this.setVar("--bg-glass-saturate", String(
|
|
551
|
+
this.setVar("--bg-glass-saturate", String(glassSaturate(Number(value))));
|
|
251
552
|
break;
|
|
252
553
|
case "wallpaperBlur":
|
|
253
554
|
this.setVar("--bg-wallpaper-blur", `${value}px`);
|
|
@@ -266,9 +567,9 @@ window.__ModuleLoader__.load({
|
|
|
266
567
|
applySurfaceGlass(settings, forceRestore) {
|
|
267
568
|
const s = document.body.style;
|
|
268
569
|
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
269
|
-
for (const token of
|
|
570
|
+
for (const token of GLASS_TOKENS) this.rememberOnce(token);
|
|
270
571
|
const restore = () => {
|
|
271
|
-
for (const token of
|
|
572
|
+
for (const token of GLASS_TOKENS) {
|
|
272
573
|
const original = this.savedTokens.get(token);
|
|
273
574
|
if (original !== void 0 && original !== "") s.setProperty(token, original);
|
|
274
575
|
else s.removeProperty(token);
|
|
@@ -276,12 +577,22 @@ window.__ModuleLoader__.load({
|
|
|
276
577
|
};
|
|
277
578
|
if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
|
|
278
579
|
restore();
|
|
580
|
+
document.body.removeAttribute(GLASS_ATTR);
|
|
279
581
|
this.setVar("--bg-glass-blur", "0px");
|
|
582
|
+
this.setVar("--bg-glass-brightness", "1");
|
|
583
|
+
s.removeProperty("--bg-glass-alpha");
|
|
584
|
+
s.removeProperty("--bg-glass-alpha-strong");
|
|
280
585
|
return;
|
|
281
586
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
587
|
+
document.body.setAttribute(GLASS_ATTR, "on");
|
|
588
|
+
const exposure = glassExposure(inDark);
|
|
589
|
+
this.setVar("--bg-glass-brightness", exposure.brightness);
|
|
590
|
+
this.setVar("--bg-glass-sheen", exposure.sheen);
|
|
591
|
+
this.setVar("--bg-glass-sheen-mid", exposure.sheenMid);
|
|
592
|
+
const base = glassBaseAlpha(settings.panelOpacity, inDark);
|
|
593
|
+
for (const { token, factor } of GLASS_SURFACE_TOKENS) s.setProperty(token, `rgba(255, 255, 255, ${(base * factor).toFixed(3)})`);
|
|
594
|
+
this.setVar("--bg-glass-alpha", base.toFixed(3));
|
|
595
|
+
this.setVar("--bg-glass-alpha-strong", Math.min(.9, base * 1.5).toFixed(3));
|
|
285
596
|
}
|
|
286
597
|
/** Remove the wallpaper + scrim layers and clear the active attribute. */
|
|
287
598
|
removeLayers() {
|
|
@@ -302,6 +613,7 @@ window.__ModuleLoader__.load({
|
|
|
302
613
|
this.removeLayers();
|
|
303
614
|
const s = document.body.style;
|
|
304
615
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
616
|
+
document.body.removeAttribute(GLASS_ATTR);
|
|
305
617
|
for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
|
|
306
618
|
for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
|
|
307
619
|
this.savedTokens.clear();
|
|
@@ -343,6 +655,7 @@ window.__ModuleLoader__.load({
|
|
|
343
655
|
"--bg-wallpaper-scale",
|
|
344
656
|
"--bg-glass-blur",
|
|
345
657
|
"--bg-glass-saturate",
|
|
658
|
+
"--bg-glass-brightness",
|
|
346
659
|
"--bg-preview-glass"
|
|
347
660
|
];
|
|
348
661
|
/**
|
|
@@ -362,11 +675,13 @@ window.__ModuleLoader__.load({
|
|
|
362
675
|
const inDark = document.body.dataset.dsDarkTheme !== void 0;
|
|
363
676
|
if (settings.panelOpacity >= 1) {
|
|
364
677
|
s.setProperty("--bg-glass-blur", "0px");
|
|
678
|
+
s.setProperty("--bg-glass-brightness", "1");
|
|
365
679
|
s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
|
|
366
680
|
return;
|
|
367
681
|
}
|
|
368
682
|
s.setProperty("--bg-glass-blur", `${settings.blur}px`);
|
|
369
|
-
s.setProperty("--bg-glass-saturate", String(
|
|
683
|
+
s.setProperty("--bg-glass-saturate", String(glassSaturate(settings.blur)));
|
|
684
|
+
s.setProperty("--bg-glass-brightness", glassExposure(inDark).brightness);
|
|
370
685
|
s.setProperty("--bg-preview-glass", glassSurfaceAlphas(settings.panelOpacity, inDark).bubble);
|
|
371
686
|
}
|
|
372
687
|
/** Remove every preview variable from an element (restores CSS defaults). */
|
|
@@ -400,7 +715,16 @@ window.__ModuleLoader__.load({
|
|
|
400
715
|
"background.contain": "完整",
|
|
401
716
|
"background.clear": "清除背景",
|
|
402
717
|
"background.uploadFailed": "上传失败,请重试",
|
|
403
|
-
"background.saveFailed": "保存失败,请重试"
|
|
718
|
+
"background.saveFailed": "保存失败,请重试",
|
|
719
|
+
"background.timeline": "会话时间线",
|
|
720
|
+
"background.timelineHint": "右侧消息导航轨,悬停展开问题列表(DeepSeek 官网风格)",
|
|
721
|
+
"timeline.railLabel": "对话时间线",
|
|
722
|
+
"timeline.roleUser": "用户",
|
|
723
|
+
"timeline.noText": "(无文本内容)",
|
|
724
|
+
"timeline.marksOnly": "★ 只看标记",
|
|
725
|
+
"timeline.mark": "标记为重点",
|
|
726
|
+
"timeline.unmark": "取消重点标记",
|
|
727
|
+
"timeline.noMarks": "还没有标记的重点"
|
|
404
728
|
};
|
|
405
729
|
/** English copy. */
|
|
406
730
|
const en = {
|
|
@@ -427,7 +751,16 @@ window.__ModuleLoader__.load({
|
|
|
427
751
|
"background.contain": "Contain",
|
|
428
752
|
"background.clear": "Clear background",
|
|
429
753
|
"background.uploadFailed": "Upload failed, try again",
|
|
430
|
-
"background.saveFailed": "Save failed, try again"
|
|
754
|
+
"background.saveFailed": "Save failed, try again",
|
|
755
|
+
"background.timeline": "Conversation timeline",
|
|
756
|
+
"background.timelineHint": "Message rail at the right edge; hover to expand the question list (DeepSeek web style)",
|
|
757
|
+
"timeline.railLabel": "Chat timeline",
|
|
758
|
+
"timeline.roleUser": "User",
|
|
759
|
+
"timeline.noText": "(no text)",
|
|
760
|
+
"timeline.marksOnly": "★ Marked only",
|
|
761
|
+
"timeline.mark": "Mark as key point",
|
|
762
|
+
"timeline.unmark": "Unmark key point",
|
|
763
|
+
"timeline.noMarks": "No marked key points"
|
|
431
764
|
};
|
|
432
765
|
//#endregion
|
|
433
766
|
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -522,6 +855,9 @@ window.__ModuleLoader__.load({
|
|
|
522
855
|
};
|
|
523
856
|
const promise = run();
|
|
524
857
|
this.pending = promise;
|
|
858
|
+
promise.then(() => {
|
|
859
|
+
if (this.pending === promise) this.pending = void 0;
|
|
860
|
+
});
|
|
525
861
|
return promise;
|
|
526
862
|
}
|
|
527
863
|
/**
|
|
@@ -549,7 +885,7 @@ window.__ModuleLoader__.load({
|
|
|
549
885
|
const settingsClient = new SettingsClient();
|
|
550
886
|
//#endregion
|
|
551
887
|
//#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
|
|
552
|
-
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.
|
|
888
|
+
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:#0f172ac7;text-shadow:0 1px 2px #fff6;white-space:nowrap;font-size:11px;line-height:1}body[data-ds-dark-theme] .-ozqdq_previewGlassText{color:#ffffffd9;text-shadow:0 1px 2px #00000059}.-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}}";
|
|
553
889
|
const tagId = "deepseek-harness-background/SettingsRow.module.css";
|
|
554
890
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
555
891
|
const tag = document.createElement("style");
|
|
@@ -559,38 +895,40 @@ window.__ModuleLoader__.load({
|
|
|
559
895
|
document.head.appendChild(tag);
|
|
560
896
|
}
|
|
561
897
|
var SettingsRow_module_css_default = {
|
|
562
|
-
"segActive": "-ozqdq_segActive",
|
|
563
898
|
"error": "-ozqdq_error",
|
|
564
|
-
"sliderLabel": "-ozqdq_sliderLabel",
|
|
565
|
-
"previewEmpty": "-ozqdq_previewEmpty",
|
|
566
|
-
"row": "-ozqdq_row",
|
|
567
899
|
"urlRow": "-ozqdq_urlRow",
|
|
568
|
-
"
|
|
569
|
-
"
|
|
900
|
+
"switch": "-ozqdq_switch",
|
|
901
|
+
"hint": "-ozqdq_hint",
|
|
902
|
+
"titleRow": "-ozqdq_titleRow",
|
|
570
903
|
"clearRow": "-ozqdq_clearRow",
|
|
571
|
-
"
|
|
572
|
-
"
|
|
573
|
-
"
|
|
574
|
-
"
|
|
904
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
905
|
+
"previewEmpty": "-ozqdq_previewEmpty",
|
|
906
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
907
|
+
"urlInput": "-ozqdq_urlInput",
|
|
908
|
+
"segGroup": "-ozqdq_segGroup",
|
|
909
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
575
910
|
"previewGlassText": "-ozqdq_previewGlassText",
|
|
576
|
-
"btnPrimary": "-ozqdq_btnPrimary",
|
|
577
|
-
"header": "-ozqdq_header",
|
|
578
911
|
"title": "-ozqdq_title",
|
|
579
|
-
"
|
|
580
|
-
"
|
|
581
|
-
"
|
|
582
|
-
"
|
|
583
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
912
|
+
"sliderValue": "-ozqdq_sliderValue",
|
|
913
|
+
"sliderLabel": "-ozqdq_sliderLabel",
|
|
914
|
+
"switchKnob": "-ozqdq_switchKnob",
|
|
915
|
+
"btn": "-ozqdq_btn",
|
|
584
916
|
"desc": "-ozqdq_desc",
|
|
585
917
|
"sliderRow": "-ozqdq_sliderRow",
|
|
918
|
+
"segRow": "-ozqdq_segRow",
|
|
919
|
+
"sectionLabel": "-ozqdq_sectionLabel",
|
|
920
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
921
|
+
"segActive": "-ozqdq_segActive",
|
|
922
|
+
"btnDanger": "-ozqdq_btnDanger",
|
|
586
923
|
"previewImg": "-ozqdq_previewImg",
|
|
587
|
-
"hint": "-ozqdq_hint",
|
|
588
924
|
"segBtn": "-ozqdq_segBtn",
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
-
"
|
|
592
|
-
"
|
|
593
|
-
"
|
|
925
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
926
|
+
"preview": "-ozqdq_preview",
|
|
927
|
+
"row": "-ozqdq_row",
|
|
928
|
+
"segLabel": "-ozqdq_segLabel",
|
|
929
|
+
"header": "-ozqdq_header",
|
|
930
|
+
"slider": "-ozqdq_slider",
|
|
931
|
+
"btnPrimary": "-ozqdq_btnPrimary",
|
|
594
932
|
"section": "-ozqdq_section"
|
|
595
933
|
};
|
|
596
934
|
//#endregion
|
|
@@ -623,7 +961,8 @@ window.__ModuleLoader__.load({
|
|
|
623
961
|
panelOpacity: DEFAULT_PANEL_OPACITY,
|
|
624
962
|
blur: 16,
|
|
625
963
|
wallpaperBlur: 0,
|
|
626
|
-
fit: DEFAULT_FIT
|
|
964
|
+
fit: DEFAULT_FIT,
|
|
965
|
+
timeline: true
|
|
627
966
|
};
|
|
628
967
|
/** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
|
|
629
968
|
* the two blur sliders feel smooth near small values. Bounds come from the
|
|
@@ -671,7 +1010,7 @@ window.__ModuleLoader__.load({
|
|
|
671
1010
|
}
|
|
672
1011
|
/** Field-wise equality for distinguishing our own saves from external changes. */
|
|
673
1012
|
function sameSettings(a, b) {
|
|
674
|
-
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;
|
|
1013
|
+
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;
|
|
675
1014
|
}
|
|
676
1015
|
/**
|
|
677
1016
|
* A stepped range control. While dragging it re-renders only itself: the value
|
|
@@ -909,6 +1248,16 @@ window.__ModuleLoader__.load({
|
|
|
909
1248
|
paintLive(next);
|
|
910
1249
|
saveNow(next);
|
|
911
1250
|
};
|
|
1251
|
+
const handleTimelineToggle = () => {
|
|
1252
|
+
markEditing();
|
|
1253
|
+
const next = {
|
|
1254
|
+
...draftRef.current,
|
|
1255
|
+
timeline: !draftRef.current.timeline
|
|
1256
|
+
};
|
|
1257
|
+
draftRef.current = next;
|
|
1258
|
+
setDraft(next);
|
|
1259
|
+
saveNow(next);
|
|
1260
|
+
};
|
|
912
1261
|
const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
|
|
913
1262
|
const hasImage = sourceUrl !== "";
|
|
914
1263
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1071,6 +1420,29 @@ window.__ModuleLoader__.load({
|
|
|
1071
1420
|
children: t(mode === "cover" ? "background.cover" : "background.contain")
|
|
1072
1421
|
}, mode))
|
|
1073
1422
|
})]
|
|
1423
|
+
}),
|
|
1424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1425
|
+
className: SettingsRow_module_css_default.segRow,
|
|
1426
|
+
children: [
|
|
1427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1428
|
+
className: SettingsRow_module_css_default.segLabel,
|
|
1429
|
+
children: t("background.timeline")
|
|
1430
|
+
}),
|
|
1431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1432
|
+
type: "button",
|
|
1433
|
+
role: "switch",
|
|
1434
|
+
"aria-checked": draft.timeline,
|
|
1435
|
+
"aria-label": t("background.timeline"),
|
|
1436
|
+
title: t("background.timelineHint"),
|
|
1437
|
+
className: SettingsRow_module_css_default.switch,
|
|
1438
|
+
onClick: handleTimelineToggle,
|
|
1439
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsRow_module_css_default.switchKnob })
|
|
1440
|
+
}),
|
|
1441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1442
|
+
className: SettingsRow_module_css_default.hint,
|
|
1443
|
+
children: t("background.timelineHint")
|
|
1444
|
+
})
|
|
1445
|
+
]
|
|
1074
1446
|
})
|
|
1075
1447
|
]
|
|
1076
1448
|
}),
|
|
@@ -1092,17 +1464,984 @@ window.__ModuleLoader__.load({
|
|
|
1092
1464
|
});
|
|
1093
1465
|
}
|
|
1094
1466
|
//#endregion
|
|
1467
|
+
//#region src/client/timeline-css.ts
|
|
1468
|
+
/**
|
|
1469
|
+
* Timeline CSS — the conversation scroll-navigation rail (DeepSeek web
|
|
1470
|
+
* ScrollNav structure, rebuilt on this plugin's frosted-glass system).
|
|
1471
|
+
*
|
|
1472
|
+
* Structure and metrics mirror the official chat.deepseek.com stylesheet
|
|
1473
|
+
* (module map extracted from the shipped bundle; v0.1.4 of the reference port
|
|
1474
|
+
* dsh-chat-timeline widens the panel to 260px and adds bookmarks):
|
|
1475
|
+
* nav 34px-wide rail, fixed right edge, vertically centered; collapsed
|
|
1476
|
+
* blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
|
|
1477
|
+
* whose scroll area holds 30px rows (tick indicator + fading-in title);
|
|
1478
|
+
* 32px gradient fade veils top/bottom, expanded-only, shown while that
|
|
1479
|
+
* side actually clips.
|
|
1480
|
+
*
|
|
1481
|
+
* Deviations from the reference port, all fidelity fixes:
|
|
1482
|
+
* - The filter bar collapses to zero height while idle instead of staying
|
|
1483
|
+
* in layout invisibly (it pushed every tick ~33px down).
|
|
1484
|
+
* - Short stacks center vertically; overflowing stacks are bottom-pinned
|
|
1485
|
+
* by the component so the newest tick hugs the capsule's bottom edge
|
|
1486
|
+
* (the official idle strategy — the reference port flowed ticks from
|
|
1487
|
+
* the top and clipped the newest away).
|
|
1488
|
+
*
|
|
1489
|
+
* Glass integration: the official chat.deepseek.com paints (light
|
|
1490
|
+
* rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
|
|
1491
|
+
* 5px collapsed / 16px expanded blur) remain as the NO-GLASS fallback.
|
|
1492
|
+
* While the plugin's glass system is on (`body[data-dsh-bg-glass]`), the
|
|
1493
|
+
* rail joins the SAME unified recipe as the composer card and bubbles:
|
|
1494
|
+
* fill = the painter's `--dsw-specific-input-major` token, blur/saturate/
|
|
1495
|
+
* brightness = the shared `--bg-glass-*` variables, i.e. the glass-blur
|
|
1496
|
+
* slider. Class prefix `dsbt-` deliberately avoids every
|
|
1497
|
+
* `[class*="_xxx"]` anchor of the plugin's glass sheet (underscores only).
|
|
1498
|
+
*/
|
|
1499
|
+
/** Unique id stamped on the injected style element (dedup key). */
|
|
1500
|
+
const TIMELINE_CSS_TAG = "deepseek-harness-background/timeline";
|
|
1501
|
+
/** Inject the timeline stylesheet once. Idempotent (dedup key on the tag). */
|
|
1502
|
+
function injectTimelineCss() {
|
|
1503
|
+
if (typeof document === "undefined") return;
|
|
1504
|
+
if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
|
|
1505
|
+
const tag = document.createElement("style");
|
|
1506
|
+
tag.dataset.plugin = "deepseek-harness-background";
|
|
1507
|
+
tag.dataset.pluginCss = TIMELINE_CSS_TAG;
|
|
1508
|
+
tag.textContent = TIMELINE_CSS;
|
|
1509
|
+
document.head.appendChild(tag);
|
|
1510
|
+
}
|
|
1511
|
+
const TIMELINE_CSS = `
|
|
1512
|
+
.dsbt-nav {
|
|
1513
|
+
user-select: none; -webkit-user-select: none;
|
|
1514
|
+
/* Filter-bar chrome height shared by the bar itself and the top veil's
|
|
1515
|
+
expanded offset. */
|
|
1516
|
+
--dsbt-bar-h: 33px;
|
|
1517
|
+
z-index: 90; align-items: center; width: 34px;
|
|
1518
|
+
height: var(--dsbt-h, 300px);
|
|
1519
|
+
transition: right .2s ease, opacity .2s ease;
|
|
1520
|
+
display: flex; position: fixed; top: 50%; right: 16px;
|
|
1521
|
+
transform: translateY(-50%); pointer-events: auto;
|
|
1522
|
+
}
|
|
1523
|
+
@media (max-width: 767px) { .dsbt-nav { display: none; } }
|
|
1524
|
+
|
|
1525
|
+
/* Collapsed capsule — frosted strip behind the tick marks. Official paint:
|
|
1526
|
+
light rgba(255,255,255,.8), dark rgba(21,21,23,.6), 5px blur. */
|
|
1527
|
+
.dsbt-bg {
|
|
1528
|
+
background-color: rgba(255, 255, 255, .8);
|
|
1529
|
+
-webkit-backdrop-filter: blur(5px);
|
|
1530
|
+
backdrop-filter: blur(5px);
|
|
1531
|
+
box-shadow:
|
|
1532
|
+
inset 0 1px 0 rgba(255, 255, 255, .28),
|
|
1533
|
+
inset 0 0 0 .5px rgba(255, 255, 255, .1),
|
|
1534
|
+
0 6px 18px rgba(0, 0, 0, .07);
|
|
1535
|
+
border-radius: 16px; width: 34px;
|
|
1536
|
+
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1537
|
+
position: absolute; top: 50%; right: 0; transform: translateY(-50%);
|
|
1538
|
+
transition: opacity .2s ease; pointer-events: none;
|
|
1539
|
+
}
|
|
1540
|
+
body[data-ds-dark-theme] .dsbt-bg {
|
|
1541
|
+
background-color: rgba(21, 21, 23, .6);
|
|
1542
|
+
box-shadow:
|
|
1543
|
+
inset 0 1px 0 rgba(255, 255, 255, .09),
|
|
1544
|
+
inset 0 0 0 .5px rgba(255, 255, 255, .05),
|
|
1545
|
+
0 6px 18px rgba(0, 0, 0, .3);
|
|
1546
|
+
}
|
|
1547
|
+
.dsbt-bg.dsbt-bg-hide { opacity: 0; }
|
|
1548
|
+
|
|
1549
|
+
/* Expanding panel — exactly the capsule's box (identical height in both
|
|
1550
|
+
states), width animates 34px -> measured fit (max 260px). Official
|
|
1551
|
+
hover-state paint: white/black frosted glass at ~94% alpha, 16px blur. */
|
|
1552
|
+
.dsbt-wrap {
|
|
1553
|
+
width: 34px;
|
|
1554
|
+
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1555
|
+
border: 1px solid transparent; border-radius: 16px;
|
|
1556
|
+
flex-direction: column; align-items: stretch;
|
|
1557
|
+
transition: width .22s cubic-bezier(.4, 0, .2, 1), background-color .2s ease,
|
|
1558
|
+
box-shadow .2s ease, border-color .2s ease;
|
|
1559
|
+
display: flex; position: absolute; top: 50%; right: 0;
|
|
1560
|
+
transform: translateY(-50%);
|
|
1561
|
+
overflow: hidden; box-sizing: border-box; background: transparent;
|
|
1562
|
+
pointer-events: none;
|
|
1563
|
+
}
|
|
1564
|
+
.dsbt-wrap.dsbt-show {
|
|
1565
|
+
pointer-events: auto;
|
|
1566
|
+
width: min(var(--dsbt-w, 260px), 260px);
|
|
1567
|
+
background-color: rgba(255, 255, 255, .94);
|
|
1568
|
+
-webkit-backdrop-filter: blur(16px);
|
|
1569
|
+
backdrop-filter: blur(16px);
|
|
1570
|
+
border-color: rgba(0, 0, 0, .06);
|
|
1571
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, .08), 0 2px 8px rgba(0, 0, 0, .04);
|
|
1572
|
+
}
|
|
1573
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show {
|
|
1574
|
+
background-color: rgba(28, 28, 32, .95);
|
|
1575
|
+
border-color: rgba(255, 255, 255, .08);
|
|
1576
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .25);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/* Content fade veils — the official ::before/::after pair rebuilt as
|
|
1580
|
+
elements: a bar of the panel's OWN fill color masked to nothing over
|
|
1581
|
+
32px (solid -> mask == gradient-to-transparent, but works with the
|
|
1582
|
+
translucent fill). They belong to the EXPANDED panel only — the idle
|
|
1583
|
+
capsule hard-clips its ticks at the rounded ends like the official
|
|
1584
|
+
strip — and while expanded the top veil starts BELOW the filter bar,
|
|
1585
|
+
not over it. Shown only while that side actually clips. */
|
|
1586
|
+
.dsbt-fade {
|
|
1587
|
+
z-index: 2; pointer-events: none; opacity: 0;
|
|
1588
|
+
background-color: rgba(255, 255, 255, .94);
|
|
1589
|
+
-webkit-mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1590
|
+
mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1591
|
+
width: 100%; height: 32px; transition: opacity .2s ease;
|
|
1592
|
+
position: absolute; left: 0; top: 0;
|
|
1593
|
+
}
|
|
1594
|
+
body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
|
|
1595
|
+
.dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
|
|
1596
|
+
.dsbt-wrap.dsbt-show .dsbt-fade.dsbt-fade-on { opacity: 1; }
|
|
1597
|
+
.dsbt-wrap.dsbt-show .dsbt-fade:not(.dsbt-fade-bot) { top: var(--dsbt-bar-h, 33px); }
|
|
1598
|
+
|
|
1599
|
+
/* Filter bar — "marked only" toggle pinned above the scroll area while
|
|
1600
|
+
expanded. Collapsed, it collapses to ZERO height (not just opacity): a
|
|
1601
|
+
transparent-but-in-layout bar pushed every tick ~33px down, one of the
|
|
1602
|
+
reasons the idle capsule read as off-alignment. */
|
|
1603
|
+
.dsbt-filterbar {
|
|
1604
|
+
height: var(--dsbt-bar-h, 33px);
|
|
1605
|
+
padding: 8px 12px 4px; display: flex; justify-content: flex-end;
|
|
1606
|
+
align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
|
|
1607
|
+
box-sizing: border-box; flex: none; overflow: hidden;
|
|
1608
|
+
transition: opacity .2s ease, height .2s ease, padding .2s ease,
|
|
1609
|
+
border-color .2s ease;
|
|
1610
|
+
}
|
|
1611
|
+
.dsbt-wrap:not(.dsbt-show) .dsbt-filterbar {
|
|
1612
|
+
height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
|
|
1613
|
+
border-bottom-color: transparent;
|
|
1614
|
+
}
|
|
1615
|
+
body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
|
|
1616
|
+
.dsbt-filterbtn {
|
|
1617
|
+
font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
|
|
1618
|
+
border: 1px solid rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .03);
|
|
1619
|
+
color: rgba(0, 0, 0, .65); cursor: pointer; transition: all .15s ease;
|
|
1620
|
+
display: inline-flex; align-items: center; gap: 3px; font-family: inherit;
|
|
1621
|
+
}
|
|
1622
|
+
.dsbt-filterbtn:hover { background: rgba(0, 0, 0, .07); color: rgba(0, 0, 0, .9); }
|
|
1623
|
+
.dsbt-filterbtn.dsbt-filteron {
|
|
1624
|
+
background: rgba(245, 158, 11, .12); border-color: #f59e0b;
|
|
1625
|
+
color: #b45309; font-weight: 600;
|
|
1626
|
+
}
|
|
1627
|
+
body[data-ds-dark-theme] .dsbt-filterbtn {
|
|
1628
|
+
border-color: rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .05);
|
|
1629
|
+
color: rgba(255, 255, 255, .7);
|
|
1630
|
+
}
|
|
1631
|
+
body[data-ds-dark-theme] .dsbt-filterbtn:hover { background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .95); }
|
|
1632
|
+
body[data-ds-dark-theme] .dsbt-filterbtn.dsbt-filteron {
|
|
1633
|
+
background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/* Scroll area — rows hug the right edge; scrolls only when overfull. The
|
|
1637
|
+
auto-margin pseudo pair vertically centers the stack when it fits the
|
|
1638
|
+
capsule (short conversations), and resolves to zero free space when it
|
|
1639
|
+
overflows, so bottom-pinned scrolling behaves like a plain column. */
|
|
1640
|
+
.dsbt-page {
|
|
1641
|
+
padding: 15px 0 15px 24px; box-sizing: border-box;
|
|
1642
|
+
overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
|
|
1643
|
+
flex-direction: column; align-items: flex-end; display: flex;
|
|
1644
|
+
position: relative; width: 100%; overflow: hidden;
|
|
1645
|
+
}
|
|
1646
|
+
.dsbt-page::before, .dsbt-page::after {
|
|
1647
|
+
content: ''; flex: none; margin-block: auto;
|
|
1648
|
+
}
|
|
1649
|
+
.dsbt-wrap.dsbt-show .dsbt-page {
|
|
1650
|
+
overflow-y: auto; overflow-x: hidden;
|
|
1651
|
+
scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
|
|
1652
|
+
}
|
|
1653
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-page { scrollbar-color: rgba(255, 255, 255, .25) transparent; }
|
|
1654
|
+
.dsbt-page::-webkit-scrollbar { width: 4px; }
|
|
1655
|
+
.dsbt-page::-webkit-scrollbar-track { background: transparent; }
|
|
1656
|
+
.dsbt-page::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, .15); border-radius: 4px; }
|
|
1657
|
+
body[data-ds-dark-theme] .dsbt-page::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .25); }
|
|
1658
|
+
|
|
1659
|
+
/* Empty state (filtered list with no marks). */
|
|
1660
|
+
.dsbt-empty {
|
|
1661
|
+
font-size: 12px; line-height: 18px; padding: 16px 12px;
|
|
1662
|
+
color: rgba(0, 0, 0, .45); text-align: center; width: 100%; box-sizing: border-box;
|
|
1663
|
+
}
|
|
1664
|
+
body[data-ds-dark-theme] .dsbt-empty { color: rgba(255, 255, 255, .45); }
|
|
1665
|
+
|
|
1666
|
+
/* One message row: 30px, star bookmark + tick indicator at the right,
|
|
1667
|
+
title fading in. */
|
|
1668
|
+
/* Row shell: layout + shared ink only — the interactive hit areas are the
|
|
1669
|
+
sibling .dsbt-jump / .dsbt-star buttons below. */
|
|
1670
|
+
.dsbt-item {
|
|
1671
|
+
height: 30px; min-height: 30px; flex-shrink: 0;
|
|
1672
|
+
justify-content: flex-end; align-items: center;
|
|
1673
|
+
width: calc(100% - 4px); margin-right: 4px;
|
|
1674
|
+
display: flex; box-sizing: border-box;
|
|
1675
|
+
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
|
|
1676
|
+
transition: color .15s ease;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* Jump target: a real button so Enter/Space work natively without nesting
|
|
1680
|
+
an interactive element inside another. */
|
|
1681
|
+
.dsbt-jump {
|
|
1682
|
+
cursor: pointer; background: none; border: none; margin: 0; padding: 0;
|
|
1683
|
+
font: inherit; line-height: 20px; text-align: right; color: inherit;
|
|
1684
|
+
flex: 1 1 auto; min-width: 0; height: 100%;
|
|
1685
|
+
display: flex; align-items: center; justify-content: flex-end;
|
|
1686
|
+
}
|
|
1687
|
+
.dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
|
|
1688
|
+
.dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
|
|
1689
|
+
body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
|
|
1690
|
+
body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
|
|
1691
|
+
.dsbt-title {
|
|
1692
|
+
font-size: 13px; line-height: 20px; flex: 1 1 auto; min-width: 0;
|
|
1693
|
+
text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
|
|
1694
|
+
margin-right: 6px; text-align: right; color: inherit;
|
|
1695
|
+
opacity: 0; transition: opacity .12s ease, color .15s ease;
|
|
1696
|
+
}
|
|
1697
|
+
.dsbt-show .dsbt-title { opacity: 1; }
|
|
1698
|
+
|
|
1699
|
+
/* Star bookmark — appears on row hover while the panel is expanded; gold
|
|
1700
|
+
when marked. A real sibling button next to the jump target, so marking
|
|
1701
|
+
stays keyboard-operable without nested interactive elements. */
|
|
1702
|
+
.dsbt-star {
|
|
1703
|
+
font-size: 13px; line-height: 16px; width: 18px; height: 18px;
|
|
1704
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1705
|
+
opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
|
|
1706
|
+
flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
|
|
1707
|
+
-webkit-user-select: none;
|
|
1708
|
+
background: none; border: none; padding: 0; font-family: inherit;
|
|
1709
|
+
}
|
|
1710
|
+
.dsbt-star:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; opacity: .9; }
|
|
1711
|
+
.dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
|
|
1712
|
+
.dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
|
|
1713
|
+
.dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
|
|
1714
|
+
body[data-ds-dark-theme] .dsbt-star { color: rgba(255, 255, 255, .25); }
|
|
1715
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star:hover { color: #fde047; }
|
|
1716
|
+
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #fbbf24; }
|
|
1717
|
+
|
|
1718
|
+
.dsbt-ind { flex-shrink: 0; justify-content: center; align-items: center; width: 16px; height: 20px; display: flex; }
|
|
1719
|
+
.dsbt-line {
|
|
1720
|
+
background-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, .16));
|
|
1721
|
+
border-radius: 4px; flex-shrink: 0; width: 8px; height: 2px;
|
|
1722
|
+
transition: background-color .2s ease, transform .2s ease, width .2s ease, height .2s ease;
|
|
1723
|
+
}
|
|
1724
|
+
.dsbt-item:hover .dsbt-line { background-color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .9)); }
|
|
1725
|
+
.dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1726
|
+
.dsbt-item.dsbt-active .dsbt-title { color: var(--dsw-alias-state-business-primary, #4d6bfe); font-weight: 500; }
|
|
1727
|
+
.dsbt-item.dsbt-active .dsbt-line {
|
|
1728
|
+
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1729
|
+
transform-origin: 50%; transform: scale(1.5);
|
|
1730
|
+
}
|
|
1731
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1732
|
+
body[data-ds-dark-theme] .dsbt-line { background-color: rgba(255, 255, 255, .2); }
|
|
1733
|
+
body[data-ds-dark-theme] .dsbt-item:hover .dsbt-line { background-color: rgba(255, 255, 255, .9); }
|
|
1734
|
+
|
|
1735
|
+
/* Marked (key-point) rows: a wider golden tick in both themes; the active
|
|
1736
|
+
state keeps its brand-blue scale so reading position always wins. */
|
|
1737
|
+
.dsbt-item.dsbt-marked .dsbt-line { background-color: #d97706; width: 10px; height: 2.5px; }
|
|
1738
|
+
.dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #b45309; }
|
|
1739
|
+
.dsbt-item.dsbt-marked.dsbt-active .dsbt-line {
|
|
1740
|
+
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1741
|
+
transform-origin: 50%; transform: scale(1.5);
|
|
1742
|
+
}
|
|
1743
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-marked .dsbt-line { background-color: #fbbf24; width: 10px; height: 2.5px; }
|
|
1744
|
+
body[data-ds-dark-theme] .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #fde047; }
|
|
1745
|
+
|
|
1746
|
+
/* ---- Unified glass integration -----------------------------------------
|
|
1747
|
+
Glass system on (wallpaper active + panelOpacity < 1): the rail joins
|
|
1748
|
+
the composer/bubble recipe — the same translucent fill token and the
|
|
1749
|
+
same blur/saturate/brightness chain (the glass-blur slider drives it,
|
|
1750
|
+
no fixed 5/16px). Placed after the dark-theme rules so equal-specificity
|
|
1751
|
+
overrides win in both schemes. */
|
|
1752
|
+
body[data-dsh-bg-glass] .dsbt-bg,
|
|
1753
|
+
body[data-dsh-bg-glass] .dsbt-wrap.dsbt-show {
|
|
1754
|
+
background-color: var(--dsw-specific-input-major);
|
|
1755
|
+
-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);
|
|
1756
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
1757
|
+
}
|
|
1758
|
+
body[data-dsh-bg-glass] .dsbt-fade {
|
|
1759
|
+
background-color: var(--dsw-specific-input-major);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1763
|
+
.dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-jump, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
|
|
1764
|
+
}
|
|
1765
|
+
`;
|
|
1766
|
+
//#endregion
|
|
1767
|
+
//#region src/client/timeline.tsx
|
|
1768
|
+
/**
|
|
1769
|
+
* Conversation timeline rail — DeepSeek web ScrollNav rebuilt on this
|
|
1770
|
+
* plugin's frosted-glass system (styles: ./timeline-css.ts).
|
|
1771
|
+
*
|
|
1772
|
+
* Collapsed: a fixed 34px-wide frosted capsule at the right edge, vertically
|
|
1773
|
+
* centered, one tick per user message (active tick scaled + brand blue,
|
|
1774
|
+
* key-point bookmarks golden). Hovering expands the SAME box into a frosted
|
|
1775
|
+
* panel listing every user question (title fades in; >8 messages widen to
|
|
1776
|
+
* 260px, otherwise the width fits the longest title). Both states share one
|
|
1777
|
+
* computed outer height, so expanding never jumps, and 32px fade veils mask
|
|
1778
|
+
* the clipped edge on each side while that side actually scrolls.
|
|
1779
|
+
*
|
|
1780
|
+
* Enhancements ported from dsh-chat-timeline v0.1.4 (MIT):
|
|
1781
|
+
* - Key-point bookmarks: star a question in the expanded panel, persisted per
|
|
1782
|
+
* session in localStorage; marked rows show a golden tick in the capsule
|
|
1783
|
+
* and a "marked only" filter narrows the list.
|
|
1784
|
+
* - Jump stabilization: the reading-position tracker freezes while a smooth
|
|
1785
|
+
* jump animates (cleared 150ms after the last scroll event, plus an 800ms
|
|
1786
|
+
* fallback), so the panel cannot jitter mid-jump.
|
|
1787
|
+
* - Rewind integration: messages withdrawn by a rewind command node are
|
|
1788
|
+
* filtered out of the rail.
|
|
1789
|
+
*
|
|
1790
|
+
* Data source, fastest first: the host-side `bgTimeline` session projection
|
|
1791
|
+
* (src/projection.ts — a complete enumeration of the session's user messages
|
|
1792
|
+
* delivered through the projection baseline/push channel) -> the loaded chat
|
|
1793
|
+
* node window. The conversation's own lazy loading is never driven from here;
|
|
1794
|
+
* clicking a row pages older history on demand, scrolls the conversation to
|
|
1795
|
+
* the target row, and keeps the rail's own active row visible.
|
|
1796
|
+
*
|
|
1797
|
+
* Collapsed idle strategy (official ScrollNav behavior): the tick stack is
|
|
1798
|
+
* pinned to the BOTTOM inner edge — the newest question's tick always sits at
|
|
1799
|
+
* the bottom of the capsule, older ticks clip away at the top. Hovering
|
|
1800
|
+
* expands the SAME box and scrolls the active row into view instead.
|
|
1801
|
+
*
|
|
1802
|
+
* The rail reads the shared settings transport: it renders unless
|
|
1803
|
+
* `timeline: false`. Its glass paints are self-contained official values
|
|
1804
|
+
* (see timeline-css.ts), independent of the plugin's token overrides.
|
|
1805
|
+
*/
|
|
1806
|
+
if (typeof document !== "undefined") injectTimelineCss();
|
|
1807
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1808
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
1809
|
+
function userTextOf(content) {
|
|
1810
|
+
if (!Array.isArray(content)) return "";
|
|
1811
|
+
let out = "";
|
|
1812
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
|
|
1813
|
+
const text = block.text;
|
|
1814
|
+
if (typeof text === "string") out += text;
|
|
1815
|
+
}
|
|
1816
|
+
return out.trim().slice(0, 80);
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Stable bookmark key for one entry: the durable message key, falling back to
|
|
1820
|
+
* the anchor seq — so marks survive history reloads. Entries with neither are
|
|
1821
|
+
* not markable (empty key).
|
|
1822
|
+
* @param m - the entry (or any shaped object).
|
|
1823
|
+
*/
|
|
1824
|
+
function markKeyOf(m) {
|
|
1825
|
+
if (m === null || typeof m !== "object") return "";
|
|
1826
|
+
const rec = m;
|
|
1827
|
+
if (typeof rec.key === "string" && rec.key !== "") return `key:${rec.key}`;
|
|
1828
|
+
return typeof rec.seq === "number" ? `seq:${rec.seq}` : "";
|
|
1829
|
+
}
|
|
1830
|
+
/** localStorage key prefix for the per-session marked-key lists. */
|
|
1831
|
+
const MARKS_STORAGE_PREFIX = "dsbt_marks_";
|
|
1832
|
+
/**
|
|
1833
|
+
* Read the marked keys for one session from localStorage.
|
|
1834
|
+
* @param sessionId - the session the marks belong to.
|
|
1835
|
+
*/
|
|
1836
|
+
function readMarks(sessionId) {
|
|
1837
|
+
if (typeof window === "undefined" || !sessionId) return [];
|
|
1838
|
+
try {
|
|
1839
|
+
const raw = window.localStorage.getItem(MARKS_STORAGE_PREFIX + sessionId);
|
|
1840
|
+
if (!raw) return [];
|
|
1841
|
+
const parsed = JSON.parse(raw);
|
|
1842
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
1843
|
+
} catch {
|
|
1844
|
+
return [];
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Persist the marked keys for one session (removes the key when empty).
|
|
1849
|
+
* @param sessionId - the session the marks belong to.
|
|
1850
|
+
* @param marks - the marked-key list.
|
|
1851
|
+
*/
|
|
1852
|
+
function writeMarks(sessionId, marks) {
|
|
1853
|
+
if (typeof window === "undefined" || !sessionId) return;
|
|
1854
|
+
try {
|
|
1855
|
+
if (!Array.isArray(marks) || marks.length === 0) window.localStorage.removeItem(MARKS_STORAGE_PREFIX + sessionId);
|
|
1856
|
+
else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
|
|
1857
|
+
} catch {}
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Extract the rewind target seq from a rewind outcome text.
|
|
1861
|
+
* @param text - the command outcome text.
|
|
1862
|
+
*/
|
|
1863
|
+
function rewindTargetOfOutcome(text) {
|
|
1864
|
+
if (typeof text !== "string") return void 0;
|
|
1865
|
+
const digits = (/rewound.*?(?:target\s+)?(\d+)/i.exec(text) ?? /#(\d+)/.exec(text))?.[1];
|
|
1866
|
+
if (digits === void 0) return void 0;
|
|
1867
|
+
const n = Number.parseInt(digits, 10);
|
|
1868
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Is this rewind command a preview/dry-run (marks the target hidden without
|
|
1872
|
+
* an outcome)?
|
|
1873
|
+
* @param command - the command payload.
|
|
1874
|
+
*/
|
|
1875
|
+
function isRewindPreviewCommand(command) {
|
|
1876
|
+
if (command.args === null || typeof command.args !== "object") return false;
|
|
1877
|
+
const args = command.args;
|
|
1878
|
+
return args.preview === true || args.dryRun === true;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Collect the seq numbers a rewind hid: preview commands hide their target
|
|
1882
|
+
* directly; successful commands hide their own row plus every node anchored
|
|
1883
|
+
* inside the rewound span [target, sourceEventSeq].
|
|
1884
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
1885
|
+
*/
|
|
1886
|
+
function hiddenSeqsOfChat(chat) {
|
|
1887
|
+
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
1888
|
+
const spans = rewindSpansOfChat(chat);
|
|
1889
|
+
if (spans.length === 0) return hidden;
|
|
1890
|
+
const nodes = chat.nodes;
|
|
1891
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
|
|
1892
|
+
for (const node of nodes.values()) {
|
|
1893
|
+
if (node === null || typeof node !== "object") continue;
|
|
1894
|
+
const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
|
|
1895
|
+
if (anchor === void 0) continue;
|
|
1896
|
+
if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
|
|
1897
|
+
}
|
|
1898
|
+
return hidden;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Seq numbers a rewind hid directly: successful commands hide their own row;
|
|
1902
|
+
* preview commands hide their target.
|
|
1903
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
1904
|
+
*/
|
|
1905
|
+
function rewindHiddenSeqsOfChat(chat) {
|
|
1906
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
1907
|
+
if (chat === null || typeof chat !== "object") return hidden;
|
|
1908
|
+
const nodes = chat.nodes;
|
|
1909
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
|
|
1910
|
+
for (const node of nodes.values()) {
|
|
1911
|
+
if (node === null || typeof node !== "object") continue;
|
|
1912
|
+
if (node.kind !== "command") continue;
|
|
1913
|
+
const command = node.data;
|
|
1914
|
+
if (command === null || typeof command !== "object") continue;
|
|
1915
|
+
if (command.name !== "rewind") continue;
|
|
1916
|
+
if (isRewindPreviewCommand(command)) {
|
|
1917
|
+
if (typeof command.seq === "number") hidden.add(command.seq);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
const outcome = command.outcome;
|
|
1921
|
+
if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
|
|
1922
|
+
if (typeof command.seq === "number") hidden.add(command.seq);
|
|
1923
|
+
}
|
|
1924
|
+
return hidden;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* The [target, sourceEventSeq] spans successful rewind commands cut — the
|
|
1928
|
+
* range form of hiding, needed to filter sources that are NOT chat nodes
|
|
1929
|
+
* (the host projection) by plain seq membership.
|
|
1930
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
1931
|
+
*/
|
|
1932
|
+
function rewindSpansOfChat(chat) {
|
|
1933
|
+
const spans = [];
|
|
1934
|
+
if (chat === null || typeof chat !== "object") return spans;
|
|
1935
|
+
const nodes = chat.nodes;
|
|
1936
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return spans;
|
|
1937
|
+
for (const node of nodes.values()) {
|
|
1938
|
+
if (node === null || typeof node !== "object") continue;
|
|
1939
|
+
if (node.kind !== "command") continue;
|
|
1940
|
+
const command = node.data;
|
|
1941
|
+
if (command === null || typeof command !== "object") continue;
|
|
1942
|
+
if (command.name !== "rewind") continue;
|
|
1943
|
+
if (isRewindPreviewCommand(command)) continue;
|
|
1944
|
+
const outcome = command.outcome;
|
|
1945
|
+
if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
|
|
1946
|
+
const marker = outcome.sourceEventSeq;
|
|
1947
|
+
if (typeof marker !== "number") continue;
|
|
1948
|
+
const target = rewindTargetOfOutcome(outcome.text);
|
|
1949
|
+
if (target !== void 0) spans.push({
|
|
1950
|
+
start: target,
|
|
1951
|
+
end: marker
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
return spans;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Enumerate user messages from the loaded chat-node snapshot (the client-side
|
|
1958
|
+
* collector; sorted by anchor sequence, rewind-hidden entries dropped).
|
|
1959
|
+
* @param snapshot - a session snapshot (loosely typed on purpose).
|
|
1960
|
+
*/
|
|
1961
|
+
function collectMessages(snapshot) {
|
|
1962
|
+
const chat = snapshot?.chat;
|
|
1963
|
+
const nodes = chat?.nodes;
|
|
1964
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return [];
|
|
1965
|
+
const hidden = hiddenSeqsOfChat(chat);
|
|
1966
|
+
const out = [];
|
|
1967
|
+
for (const node of nodes.values()) {
|
|
1968
|
+
if (node === null || typeof node !== "object") continue;
|
|
1969
|
+
if (node.kind !== "user") continue;
|
|
1970
|
+
const data = node.data;
|
|
1971
|
+
if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
1972
|
+
const key = typeof node.key === "string" ? node.key : void 0;
|
|
1973
|
+
if (key === void 0) continue;
|
|
1974
|
+
const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
|
|
1975
|
+
if (hidden.has(seq)) continue;
|
|
1976
|
+
out.push({
|
|
1977
|
+
seq,
|
|
1978
|
+
time: data.time,
|
|
1979
|
+
text: userTextOf(data.content),
|
|
1980
|
+
key
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
1984
|
+
return out;
|
|
1985
|
+
}
|
|
1986
|
+
/** Resolve the DOM anchor key of one entry (direct key form only). */
|
|
1987
|
+
function resolveAnchorKey(m) {
|
|
1988
|
+
return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
|
|
1989
|
+
}
|
|
1990
|
+
/** The user-message node definition kind in ui-conversation. */
|
|
1991
|
+
const INPUT_MESSAGE_KIND = "input-message";
|
|
1992
|
+
/**
|
|
1993
|
+
* Rebuild a chat row's DOM anchor key from the durable message id — the same
|
|
1994
|
+
* engine formula as conversationContextKey(kind, id) for the input-message
|
|
1995
|
+
* definition (`13:input-message<id>`), so projected entries can jump even
|
|
1996
|
+
* when their chat node is not loaded yet.
|
|
1997
|
+
* @param id - the durable message id carried by the projection entry.
|
|
1998
|
+
*/
|
|
1999
|
+
function inputAnchorKeyOf(id) {
|
|
2000
|
+
return `13:${INPUT_MESSAGE_KIND}${id}`;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Validate one projected wire value into rail entries (defensive: the value
|
|
2004
|
+
* crosses the wire and may be absent, partial, or stale).
|
|
2005
|
+
* @param value - the raw useProjection(TIMELINE_PROJECTION_KEY) snapshot.
|
|
2006
|
+
*/
|
|
2007
|
+
function normalizeProjectedTimeline(value) {
|
|
2008
|
+
if (value === null || typeof value !== "object") return [];
|
|
2009
|
+
const raw = value.messages;
|
|
2010
|
+
if (!Array.isArray(raw)) return [];
|
|
2011
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2012
|
+
const out = [];
|
|
2013
|
+
for (const item of raw) {
|
|
2014
|
+
if (item === null || typeof item !== "object") continue;
|
|
2015
|
+
const rec = item;
|
|
2016
|
+
if (typeof rec.seq !== "number" || !Number.isFinite(rec.seq)) continue;
|
|
2017
|
+
if (seen.has(rec.seq)) continue;
|
|
2018
|
+
seen.add(rec.seq);
|
|
2019
|
+
out.push({
|
|
2020
|
+
seq: rec.seq,
|
|
2021
|
+
time: typeof rec.time === "number" ? rec.time : 0,
|
|
2022
|
+
text: typeof rec.text === "string" ? rec.text.slice(0, 80) : "",
|
|
2023
|
+
...typeof rec.id === "string" && rec.id !== "" ? { key: inputAnchorKeyOf(rec.id) } : {}
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
2027
|
+
return out;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Rail messages with the fastest source first: the host projection covers the
|
|
2031
|
+
* WHOLE session (and already excludes rewind cuts), so it wins whenever it
|
|
2032
|
+
* has anything; otherwise fall back to the loaded chat-node window. The
|
|
2033
|
+
* projection path still applies locally-known rewind hiding so a cut lands
|
|
2034
|
+
* instantly even before the next projection push arrives.
|
|
2035
|
+
* @param snapshot - the session conversation snapshot (node fallback).
|
|
2036
|
+
* @param projected - the raw bgTimeline projection value.
|
|
2037
|
+
*/
|
|
2038
|
+
function railMessages(snapshot, projected) {
|
|
2039
|
+
const projectedMessages = normalizeProjectedTimeline(projected);
|
|
2040
|
+
if (projectedMessages.length > 0) {
|
|
2041
|
+
const chat = snapshot?.chat;
|
|
2042
|
+
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
2043
|
+
const spans = rewindSpansOfChat(chat);
|
|
2044
|
+
if (hidden.size === 0 && spans.length === 0) return projectedMessages;
|
|
2045
|
+
return projectedMessages.filter((m) => !hidden.has(m.seq) && !spans.some((span) => m.seq >= span.start && m.seq <= span.end));
|
|
2046
|
+
}
|
|
2047
|
+
return collectMessages(snapshot);
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Outer rail height for a message count: rows + breathing room, clamped so
|
|
2051
|
+
* both states always share the exact same height (never jumps on expand).
|
|
2052
|
+
* @param count - number of user messages.
|
|
2053
|
+
*/
|
|
2054
|
+
function railHeightFor(count) {
|
|
2055
|
+
return Math.max(140, Math.min(300, count * 30 + 38));
|
|
2056
|
+
}
|
|
2057
|
+
/** Longest-title width estimate bounds. */
|
|
2058
|
+
const RAIL_MIN_WIDTH = 96;
|
|
2059
|
+
const RAIL_MAX_WIDTH = 260;
|
|
2060
|
+
/**
|
|
2061
|
+
* Measure the expanded-panel width for the given titles (canvas text metrics;
|
|
2062
|
+
* falls back to the max width when measurement is unavailable). Accounts for
|
|
2063
|
+
* the star column so a marked row never clips its tick.
|
|
2064
|
+
* @param texts - the message previews.
|
|
2065
|
+
*/
|
|
2066
|
+
function railWidthFor(texts) {
|
|
2067
|
+
if (texts.length > 8) return RAIL_MAX_WIDTH;
|
|
2068
|
+
let widest = 0;
|
|
2069
|
+
if (typeof document !== "undefined") {
|
|
2070
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
2071
|
+
if (ctx) {
|
|
2072
|
+
ctx.font = `13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
|
|
2073
|
+
for (const t of texts) {
|
|
2074
|
+
const w = ctx.measureText(t === "" ? "…" : t).width;
|
|
2075
|
+
if (w > widest) widest = w;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
|
|
2080
|
+
}
|
|
2081
|
+
/** Ensure the target node is loaded, then scroll its row into view. */
|
|
2082
|
+
async function jumpToMessage(sessionsService, sessionId, key) {
|
|
2083
|
+
const session = sessionsService.binding(sessionId)?.session;
|
|
2084
|
+
if (session === void 0) return false;
|
|
2085
|
+
let guard = 0;
|
|
2086
|
+
while (guard++ < 120) {
|
|
2087
|
+
const snapshot = session.getSnapshot();
|
|
2088
|
+
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
2089
|
+
if (snapshot?.hasMore !== true) return false;
|
|
2090
|
+
if (snapshot.loadingOlder === true) {
|
|
2091
|
+
await delay(50);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
await session.loadOlder();
|
|
2095
|
+
}
|
|
2096
|
+
const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
|
|
2097
|
+
if (row === null) return false;
|
|
2098
|
+
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2099
|
+
row.scrollIntoView({
|
|
2100
|
+
behavior: reducedMotion ? "auto" : "smooth",
|
|
2101
|
+
block: "center"
|
|
2102
|
+
});
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Scroll only the rail itself so the active row stays visible (min distance).
|
|
2107
|
+
* offsetTop math instead of getBoundingClientRect: stable while the panel's
|
|
2108
|
+
* own smooth scroll animates (no feedback loop with live rects).
|
|
2109
|
+
*/
|
|
2110
|
+
function scrollTimelineItemIntoView(page, item) {
|
|
2111
|
+
const pageTop = page.scrollTop;
|
|
2112
|
+
const pageBottom = pageTop + page.clientHeight;
|
|
2113
|
+
const itemTop = item.offsetTop;
|
|
2114
|
+
const itemBottom = itemTop + item.offsetHeight;
|
|
2115
|
+
if (itemTop < pageTop) page.scrollTop = Math.max(0, itemTop - 10);
|
|
2116
|
+
else if (itemBottom > pageBottom) page.scrollTop = itemBottom - page.clientHeight + 10;
|
|
2117
|
+
}
|
|
2118
|
+
/** Is another timeline rail already mounted (e.g. dsh-chat-timeline)? */
|
|
2119
|
+
function otherRailPresent() {
|
|
2120
|
+
return document.querySelector(".dsct_nav") !== null;
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* The timeline rail component. Portaled to body; registered into the
|
|
2124
|
+
* conversation input dock purely to bind per-session lifecycle.
|
|
2125
|
+
*/
|
|
2126
|
+
function TimelineRail(props) {
|
|
2127
|
+
const { sessionId, sessionsService, useProjection, t } = props;
|
|
2128
|
+
const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
|
|
2129
|
+
const timelineUnknown = settings.status === "loading";
|
|
2130
|
+
const timelineDisabled = settings.value?.timeline === false;
|
|
2131
|
+
const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
|
|
2132
|
+
const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
|
|
2133
|
+
const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
|
|
2134
|
+
const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
|
|
2135
|
+
const projected = useProjection !== void 0 ? useProjection(TIMELINE_PROJECTION_KEY) : void 0;
|
|
2136
|
+
const messages = react.default.useMemo(() => railMessages(nodeSnapshot, projected), [nodeSnapshot, projected]);
|
|
2137
|
+
const [activeIndex, setActiveIndex] = react.default.useState(-1);
|
|
2138
|
+
const [show, setShow] = react.default.useState(false);
|
|
2139
|
+
const [rightOffset, setRightOffset] = react.default.useState(16);
|
|
2140
|
+
const [railWidth, setRailWidth] = react.default.useState(RAIL_MAX_WIDTH);
|
|
2141
|
+
const [fades, setFades] = react.default.useState({
|
|
2142
|
+
top: false,
|
|
2143
|
+
bottom: false
|
|
2144
|
+
});
|
|
2145
|
+
const [hidden, setHidden] = react.default.useState(() => otherRailPresent());
|
|
2146
|
+
const [marks, setMarks] = react.default.useState(() => readMarks(sessionId));
|
|
2147
|
+
const [marksOnly, setMarksOnly] = react.default.useState(false);
|
|
2148
|
+
const [isNarrow, setIsNarrow] = react.default.useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches);
|
|
2149
|
+
const pageRef = react.default.useRef(null);
|
|
2150
|
+
const activeItemRef = react.default.useRef(null);
|
|
2151
|
+
const jumpPendingRef = react.default.useRef(false);
|
|
2152
|
+
const jumpFallbackRef = react.default.useRef(void 0);
|
|
2153
|
+
react.default.useEffect(() => {
|
|
2154
|
+
setMarks(readMarks(sessionId));
|
|
2155
|
+
setMarksOnly(false);
|
|
2156
|
+
}, [sessionId]);
|
|
2157
|
+
const markedSet = react.default.useMemo(() => new Set(marks), [marks]);
|
|
2158
|
+
const marksRef = react.default.useRef(marks);
|
|
2159
|
+
marksRef.current = marks;
|
|
2160
|
+
const toggleMark = react.default.useCallback((m) => {
|
|
2161
|
+
const k = markKeyOf(m);
|
|
2162
|
+
if (k === "") return;
|
|
2163
|
+
const prev = marksRef.current;
|
|
2164
|
+
const next = prev.includes(k) ? prev.filter((x) => x !== k) : [...prev, k];
|
|
2165
|
+
marksRef.current = next;
|
|
2166
|
+
setMarks(next);
|
|
2167
|
+
writeMarks(sessionId, next);
|
|
2168
|
+
}, [sessionId]);
|
|
2169
|
+
react.default.useEffect(() => {
|
|
2170
|
+
if (typeof window.matchMedia !== "function") return;
|
|
2171
|
+
const mql = window.matchMedia("(max-width: 767px)");
|
|
2172
|
+
const onChange = (e) => setIsNarrow(e.matches);
|
|
2173
|
+
mql.addEventListener("change", onChange);
|
|
2174
|
+
return () => mql.removeEventListener("change", onChange);
|
|
2175
|
+
}, []);
|
|
2176
|
+
react.default.useEffect(() => {
|
|
2177
|
+
setRailWidth(railWidthFor(messages.map((m) => m.text)));
|
|
2178
|
+
}, [messages]);
|
|
2179
|
+
react.default.useEffect(() => {
|
|
2180
|
+
if (messages.length === 0) return;
|
|
2181
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
2182
|
+
messages.forEach((m, i) => {
|
|
2183
|
+
const key = resolveAnchorKey(m);
|
|
2184
|
+
if (key !== void 0) indexByKey.set(key, i);
|
|
2185
|
+
});
|
|
2186
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2187
|
+
if (sp === null) return;
|
|
2188
|
+
const rows = sp.querySelectorAll("[data-chat-anchor-key]");
|
|
2189
|
+
let settleTimer;
|
|
2190
|
+
let frame = 0;
|
|
2191
|
+
const nearRows = /* @__PURE__ */ new Set();
|
|
2192
|
+
const updateActive = () => {
|
|
2193
|
+
if (jumpPendingRef.current) return;
|
|
2194
|
+
const rect = sp.getBoundingClientRect();
|
|
2195
|
+
if (rect.height === 0) return;
|
|
2196
|
+
const candidates = nearRows.size > 0 ? nearRows : rows;
|
|
2197
|
+
const line = rect.top + rect.height * .4;
|
|
2198
|
+
let best = -1;
|
|
2199
|
+
let bestDist = Infinity;
|
|
2200
|
+
for (const row of candidates) {
|
|
2201
|
+
const key = row.getAttribute("data-chat-anchor-key");
|
|
2202
|
+
if (key === null) continue;
|
|
2203
|
+
const idx = indexByKey.get(key) ?? -1;
|
|
2204
|
+
if (idx === -1) continue;
|
|
2205
|
+
const r = row.getBoundingClientRect();
|
|
2206
|
+
const dist = Math.abs(r.top + r.height / 2 - line);
|
|
2207
|
+
if (dist < bestDist) {
|
|
2208
|
+
bestDist = dist;
|
|
2209
|
+
best = idx;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
setActiveIndex(best);
|
|
2213
|
+
};
|
|
2214
|
+
const scheduleMeasure = () => {
|
|
2215
|
+
if (frame !== 0) return;
|
|
2216
|
+
frame = window.requestAnimationFrame(() => {
|
|
2217
|
+
frame = 0;
|
|
2218
|
+
updateActive();
|
|
2219
|
+
});
|
|
2220
|
+
};
|
|
2221
|
+
let io;
|
|
2222
|
+
if (typeof IntersectionObserver === "function") {
|
|
2223
|
+
io = new IntersectionObserver((entries) => {
|
|
2224
|
+
for (const entry of entries) if (entry.isIntersecting) nearRows.add(entry.target);
|
|
2225
|
+
else nearRows.delete(entry.target);
|
|
2226
|
+
scheduleMeasure();
|
|
2227
|
+
}, { root: sp });
|
|
2228
|
+
rows.forEach((row) => io?.observe(row));
|
|
2229
|
+
}
|
|
2230
|
+
const onScroll = () => {
|
|
2231
|
+
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2232
|
+
settleTimer = window.setTimeout(() => {
|
|
2233
|
+
settleTimer = void 0;
|
|
2234
|
+
if (jumpPendingRef.current) {
|
|
2235
|
+
jumpPendingRef.current = false;
|
|
2236
|
+
if (jumpFallbackRef.current !== void 0) {
|
|
2237
|
+
clearTimeout(jumpFallbackRef.current);
|
|
2238
|
+
jumpFallbackRef.current = void 0;
|
|
2239
|
+
}
|
|
2240
|
+
updateActive();
|
|
2241
|
+
}
|
|
2242
|
+
}, 150);
|
|
2243
|
+
scheduleMeasure();
|
|
2244
|
+
};
|
|
2245
|
+
sp.addEventListener("scroll", onScroll, { passive: true });
|
|
2246
|
+
const interval = window.setInterval(scheduleMeasure, 2e3);
|
|
2247
|
+
scheduleMeasure();
|
|
2248
|
+
return () => {
|
|
2249
|
+
io?.disconnect();
|
|
2250
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
2251
|
+
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2252
|
+
if (jumpFallbackRef.current !== void 0) {
|
|
2253
|
+
clearTimeout(jumpFallbackRef.current);
|
|
2254
|
+
jumpFallbackRef.current = void 0;
|
|
2255
|
+
}
|
|
2256
|
+
sp.removeEventListener("scroll", onScroll);
|
|
2257
|
+
clearInterval(interval);
|
|
2258
|
+
};
|
|
2259
|
+
}, [messages]);
|
|
2260
|
+
const syncFades = react.default.useCallback(() => {
|
|
2261
|
+
const page = pageRef.current;
|
|
2262
|
+
if (page === null) {
|
|
2263
|
+
setFades({
|
|
2264
|
+
top: false,
|
|
2265
|
+
bottom: false
|
|
2266
|
+
});
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
const canTop = page.scrollTop > 2;
|
|
2270
|
+
const canBot = page.scrollTop + page.clientHeight < page.scrollHeight - 2;
|
|
2271
|
+
setFades((prev) => prev.top === canTop && prev.bottom === canBot ? prev : {
|
|
2272
|
+
top: canTop,
|
|
2273
|
+
bottom: canBot
|
|
2274
|
+
});
|
|
2275
|
+
}, []);
|
|
2276
|
+
react.default.useEffect(syncFades, [
|
|
2277
|
+
syncFades,
|
|
2278
|
+
show,
|
|
2279
|
+
messages.length,
|
|
2280
|
+
marksOnly
|
|
2281
|
+
]);
|
|
2282
|
+
react.default.useLayoutEffect(() => {
|
|
2283
|
+
if (!show) return;
|
|
2284
|
+
const page = pageRef.current;
|
|
2285
|
+
const item = activeItemRef.current;
|
|
2286
|
+
if (page === null || item === null) return;
|
|
2287
|
+
scrollTimelineItemIntoView(page, item);
|
|
2288
|
+
syncFades();
|
|
2289
|
+
}, [
|
|
2290
|
+
activeIndex,
|
|
2291
|
+
messages.length,
|
|
2292
|
+
marksOnly,
|
|
2293
|
+
show,
|
|
2294
|
+
syncFades
|
|
2295
|
+
]);
|
|
2296
|
+
const height = railHeightFor(messages.length);
|
|
2297
|
+
react.default.useLayoutEffect(() => {
|
|
2298
|
+
if (show) return;
|
|
2299
|
+
const page = pageRef.current;
|
|
2300
|
+
if (page === null) return;
|
|
2301
|
+
page.scrollTop = page.scrollHeight;
|
|
2302
|
+
}, [
|
|
2303
|
+
show,
|
|
2304
|
+
messages.length,
|
|
2305
|
+
marksOnly,
|
|
2306
|
+
height
|
|
2307
|
+
]);
|
|
2308
|
+
react.default.useEffect(() => {
|
|
2309
|
+
let raf = 0;
|
|
2310
|
+
const measure = () => {
|
|
2311
|
+
raf = 0;
|
|
2312
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2313
|
+
if (sp === null) return;
|
|
2314
|
+
const rect = sp.getBoundingClientRect();
|
|
2315
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
2316
|
+
const next = Math.max(8, Math.round(window.innerWidth - rect.right + 12));
|
|
2317
|
+
setRightOffset((prev) => Math.abs(prev - next) > .5 ? next : prev);
|
|
2318
|
+
};
|
|
2319
|
+
const schedule = () => {
|
|
2320
|
+
if (raf === 0) raf = window.requestAnimationFrame(measure);
|
|
2321
|
+
};
|
|
2322
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2323
|
+
const ro = typeof ResizeObserver === "function" && sp !== null ? new ResizeObserver(schedule) : null;
|
|
2324
|
+
if (ro !== null && sp !== null) ro.observe(sp);
|
|
2325
|
+
window.addEventListener("resize", schedule);
|
|
2326
|
+
const guard = window.setInterval(() => setHidden(otherRailPresent()), 2e3);
|
|
2327
|
+
measure();
|
|
2328
|
+
return () => {
|
|
2329
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
2330
|
+
ro?.disconnect();
|
|
2331
|
+
window.removeEventListener("resize", schedule);
|
|
2332
|
+
clearInterval(guard);
|
|
2333
|
+
};
|
|
2334
|
+
}, []);
|
|
2335
|
+
if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length === 0) return null;
|
|
2336
|
+
const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
|
|
2337
|
+
const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
|
|
2338
|
+
return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2339
|
+
className: "dsbt-nav",
|
|
2340
|
+
style: {
|
|
2341
|
+
right: rightOffset,
|
|
2342
|
+
["--dsbt-h"]: `${height}px`,
|
|
2343
|
+
["--dsbt-w"]: `${railWidth}px`
|
|
2344
|
+
},
|
|
2345
|
+
role: "navigation",
|
|
2346
|
+
"aria-label": t("timeline.railLabel"),
|
|
2347
|
+
onMouseEnter: () => setShow(true),
|
|
2348
|
+
onMouseLeave: () => setShow(false),
|
|
2349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: show ? "dsbt-bg dsbt-bg-hide" : "dsbt-bg" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2350
|
+
className: show ? "dsbt-wrap dsbt-show" : "dsbt-wrap",
|
|
2351
|
+
children: [
|
|
2352
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2353
|
+
className: "dsbt-filterbar",
|
|
2354
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2355
|
+
type: "button",
|
|
2356
|
+
className: marksOnly ? "dsbt-filterbtn dsbt-filteron" : "dsbt-filterbtn",
|
|
2357
|
+
"aria-pressed": marksOnly,
|
|
2358
|
+
onClick: () => setMarksOnly((v) => !v),
|
|
2359
|
+
children: [t("timeline.marksOnly"), marks.length > 0 ? ` (${marks.length})` : ""]
|
|
2360
|
+
})
|
|
2361
|
+
}),
|
|
2362
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2363
|
+
className: fades.top ? "dsbt-fade dsbt-fade-on" : "dsbt-fade",
|
|
2364
|
+
"aria-hidden": true
|
|
2365
|
+
}),
|
|
2366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2367
|
+
className: fades.bottom ? "dsbt-fade dsbt-fade-bot dsbt-fade-on" : "dsbt-fade dsbt-fade-bot",
|
|
2368
|
+
"aria-hidden": true
|
|
2369
|
+
}),
|
|
2370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2371
|
+
className: "dsbt-page",
|
|
2372
|
+
ref: pageRef,
|
|
2373
|
+
onScroll: syncFades,
|
|
2374
|
+
children: displayMessages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2375
|
+
className: "dsbt-empty",
|
|
2376
|
+
children: t("timeline.noMarks")
|
|
2377
|
+
}) : displayMessages.map((m) => {
|
|
2378
|
+
const key = resolveAnchorKey(m);
|
|
2379
|
+
const mk = markKeyOf(m);
|
|
2380
|
+
const marked = markedSet.has(mk);
|
|
2381
|
+
const isActive = activeKey !== "" && mk === activeKey;
|
|
2382
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2383
|
+
className: `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`,
|
|
2384
|
+
children: [
|
|
2385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2386
|
+
type: "button",
|
|
2387
|
+
className: "dsbt-jump",
|
|
2388
|
+
ref: isActive ? activeItemRef : void 0,
|
|
2389
|
+
title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
|
|
2390
|
+
"aria-label": `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
|
|
2391
|
+
"aria-current": isActive ? "location" : void 0,
|
|
2392
|
+
onClick: () => {
|
|
2393
|
+
if (key === void 0) return;
|
|
2394
|
+
jumpPendingRef.current = true;
|
|
2395
|
+
if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
|
|
2396
|
+
jumpFallbackRef.current = window.setTimeout(() => {
|
|
2397
|
+
jumpFallbackRef.current = void 0;
|
|
2398
|
+
jumpPendingRef.current = false;
|
|
2399
|
+
}, 800);
|
|
2400
|
+
jumpToMessage(sessionsService, sessionId, key).catch(() => {});
|
|
2401
|
+
},
|
|
2402
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2403
|
+
className: "dsbt-title",
|
|
2404
|
+
children: m.text === "" ? t("timeline.noText") : m.text
|
|
2405
|
+
})
|
|
2406
|
+
}),
|
|
2407
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2408
|
+
type: "button",
|
|
2409
|
+
className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
|
|
2410
|
+
"aria-pressed": marked,
|
|
2411
|
+
"aria-label": marked ? t("timeline.unmark") : t("timeline.mark"),
|
|
2412
|
+
onClick: () => toggleMark(m),
|
|
2413
|
+
children: "★"
|
|
2414
|
+
}),
|
|
2415
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2416
|
+
className: "dsbt-ind",
|
|
2417
|
+
"aria-hidden": true,
|
|
2418
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsbt-line" })
|
|
2419
|
+
})
|
|
2420
|
+
]
|
|
2421
|
+
}, m.seq);
|
|
2422
|
+
})
|
|
2423
|
+
})
|
|
2424
|
+
]
|
|
2425
|
+
})]
|
|
2426
|
+
}), document.body);
|
|
2427
|
+
}
|
|
2428
|
+
//#endregion
|
|
1095
2429
|
//#region src/client/index.ts
|
|
1096
2430
|
/**
|
|
1097
|
-
* Service injections this entry declares:
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
2431
|
+
* Service injections this entry declares: `slots` + `locale` for the settings
|
|
2432
|
+
* surface and the timeline slot seat, and `sessions` so the timeline rail can
|
|
2433
|
+
* enumerate user messages and jump between them. The settings surface itself
|
|
2434
|
+
* talks to the plugin's own host route over plain same-origin fetch.
|
|
1100
2435
|
*/
|
|
1101
|
-
const inject = [
|
|
2436
|
+
const inject = [
|
|
2437
|
+
"slots",
|
|
2438
|
+
"locale",
|
|
2439
|
+
"sessions"
|
|
2440
|
+
];
|
|
1102
2441
|
/**
|
|
1103
|
-
* Client plugin body: paint the persisted background live
|
|
1104
|
-
* General-settings row.
|
|
1105
|
-
* @param ctx - client cordis context (slots/locale injected).
|
|
2442
|
+
* Client plugin body: paint the persisted background live, register the
|
|
2443
|
+
* General-settings row, and mount the conversation timeline rail.
|
|
2444
|
+
* @param ctx - client cordis context (slots/locale/sessions injected).
|
|
1106
2445
|
*/
|
|
1107
2446
|
function apply(ctx) {
|
|
1108
2447
|
const paint = () => {
|
|
@@ -1126,6 +2465,13 @@ window.__ModuleLoader__.load({
|
|
|
1126
2465
|
order: 600,
|
|
1127
2466
|
locale: "ui-background"
|
|
1128
2467
|
}, BackgroundSettingsRow));
|
|
2468
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
2469
|
+
name: "conversation.input.dock",
|
|
2470
|
+
id: "deepseek-harness-background.timeline",
|
|
2471
|
+
order: 45,
|
|
2472
|
+
locale: "ui-background",
|
|
2473
|
+
inject: () => ({ sessionsService: ctx.sessions })
|
|
2474
|
+
}, TimelineRail));
|
|
1129
2475
|
ctx.effect(() => () => {
|
|
1130
2476
|
unsubscribe();
|
|
1131
2477
|
backgroundPainter.dispose();
|