dsh-deepseek-style-theme 1.43.9 → 1.43.10

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.
Files changed (2) hide show
  1. package/lib/client.js +2180 -2173
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -1,2173 +1,2180 @@
1
- window.__ModuleLoader__.load({
2
- id: "@dsh-external/dsh-deepseek-style-theme",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- const React = require("react");
8
-
9
- // ── design-standard conformance (创造模式 preset) ────────────────────
10
- // The product shell ships build-time hashed class names (e.g.
11
- // `wSkVaW_header`). They are not API and change on every product rebuild,
12
- // so this theme treats every rule that targets them as a guarded patch
13
- // layer: each block is injected only while its anchor class actually
14
- // exists in the DOM (a 30s wait covers late-rendered shells), and a block
15
- // whose anchor never appears is skipped with a one-line diagnostic
16
- // instead of silently shipping dead CSS.
17
- // Dark styling keys off our own `data-dshome-dark` attribute, driven by
18
- // the theme service's `active.colorScheme` — never the product's
19
- // attribute name — so a product-side rename cannot break dark mode.
20
- // UI that the product exposes through a Slot (the locale toggle) stays on
21
- // the Slot; only surfaces with no Slot use DOM patches.
22
- //
23
- // Markup sinks: `innerHTML` appears exactly five times in this file and
24
- // every one of them assigns a module-level or call-site `const` SVG
25
- // literal. It is this file's only markup sink, so anything derived from
26
- // page data — session titles, file paths, workspace names — must go
27
- // through `textContent`/`title`/`setAttribute`, never `innerHTML`. An
28
- // interpolated value there would be DOM XSS.
29
- const PLUGIN_ID = "@dsh-external/dsh-deepseek-style-theme";
30
-
31
- // CORE_CSS: ours only (`:root` tokens, `html/body`, keyframes, stable
32
- // `[role=menu]`, `.dshome-*`). No product class names — always injected.
33
- // Color model: `data-dshome-color` ∈ {green(base), red(鲜红 peak), blue}
34
- // re-tints every var(--ds-brand*) consumer.
35
- const CORE_CSS = `
36
- :root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
37
- html,body{background-color:transparent!important}
38
- body{background:radial-gradient(1200px 700px at 12% -10%,color-mix(in srgb,var(--ds-brand) 14%,transparent),transparent 60%),radial-gradient(1000px 600px at 105% 12%,color-mix(in srgb,var(--ds-brand-light) 16%,transparent),transparent 55%),radial-gradient(900px 700px at 50% 115%,color-mix(in srgb,var(--ds-brand) 10%,transparent),transparent 60%),linear-gradient(180deg,#fbfbfd 0%,#f5f6fa 100%);background-attachment:fixed}
39
- body[data-dshome-color=red]{--ds-brand:#F5222D;--ds-brand-deep:#CF1322;--ds-brand-mid:#FF4D4F;--ds-brand-light:#FF7875}
40
- body[data-dshome-color=blue]{--ds-brand:#4D6BFE;--ds-brand-deep:#3A65C2;--ds-brand-mid:#4176E6;--ds-brand-light:#73A3D2}
41
- body[data-dshome-dark]{--ds-brand:#10B981;--ds-brand-deep:#059669;--ds-brand-mid:#34D399;--ds-brand-light:#6EE7B7;background:radial-gradient(1100px 650px at 8% -8%,#0a3a28 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#0d4d35 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#06291c 0,transparent 60%),linear-gradient(180deg,#081b14 0%,#0a241a 55%,#081b14 100%);background-attachment:fixed}
42
- body[data-dshome-dark][data-dshome-color=red]{--ds-brand:#FF4D4F;--ds-brand-deep:#F5222D;--ds-brand-mid:#FF7875;--ds-brand-light:#FFA39E;background:radial-gradient(1100px 650px at 8% -8%,#5c0f14 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#7d1a20 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#3f0a0e 0,transparent 60%),linear-gradient(180deg,#260709 0%,#330a0d 55%,#260709 100%);background-attachment:fixed}
43
- body[data-dshome-dark][data-dshome-color=blue]{--ds-brand:#5D79FF;--ds-brand-deep:#4D6BFE;--ds-brand-mid:#7A96FF;--ds-brand-light:#9DB9FF;background:radial-gradient(1100px 650px at 8% -8%,#1a3870 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#2d5f9e 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#0d1f4a 0,transparent 60%),linear-gradient(180deg,#0a1a3a 0%,#0d1f4a 55%,#0a1a3a 100%);background-attachment:fixed}
44
- @property --dsh-border-angle{syntax:"<angle>";initial-value:0deg;inherits:false}
45
- @keyframes dsh-border-spin{to{--dsh-border-angle:360deg}}
46
- @keyframes dshome-view-enter{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
47
- @keyframes dshome-pop-in{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:none}}
48
- @keyframes dshome-fade-in{from{opacity:0}to{opacity:1}}
49
- @keyframes dshome-tab-in{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
50
- @keyframes dshome-modal-in{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:none}}
51
- [role=menu]{animation:dshome-pop-in .22s cubic-bezier(.2,.8,.2,1)}
52
- .dshome-ws-open-wrap{padding:2px 0}
53
- .dshome-ws-open{display:flex;align-items:center;gap:8px;width:100%;min-width:170px;padding:6px 10px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease}
54
- .dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)!important;color:var(--ds-brand-deep)!important}
55
- .dshome-ws-open-icon{display:inline-flex;flex:none;color:var(--ds-brand)}
56
- .dshome-ws-open-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
57
- body[data-dshome-dark] .dshome-ws-open{color:#eef2ff}
58
- body[data-dshome-dark] .dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent)!important;color:#fff!important}
59
- body[data-dshome-dark] .dshome-ws-open-icon{color:var(--ds-brand-light)}
60
- .dshome-dstt{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}
61
- .dshome-dstt h3{margin:0;font-size:16px;font-weight:500;line-height:24px;display:flex;align-items:center;gap:8px}
62
- .dshome-dstt-tag{flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand);border-radius:999px;padding:1px 8px;font-size:11px;line-height:18px;font-weight:600}
63
- body[data-dshome-dark] .dshome-dstt-tag{color:var(--ds-brand-light)}
64
- .dshome-dstt p{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
65
- .dshome-dstt-row{display:flex;align-items:center;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;background:var(--dsw-alias-bg-layer-1)}
66
- .dshome-dstt-row-body{flex:1;min-width:0}
67
- .dshome-dstt-row-title{font-size:14px;line-height:22px;font-weight:500}
68
- .dshome-dstt-row-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
69
- .dshome-dstt-status{flex:none;font-size:12px;line-height:18px;padding:4px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
70
- .dshome-dstt-status[data-peak=true]{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep);border:1px solid color-mix(in srgb,var(--ds-brand) 30%,transparent)}
71
- .dshome-dstt-status[data-peak=false]{background:color-mix(in srgb,var(--ds-brand) 8%,transparent);color:var(--ds-brand);border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent)}
72
- body[data-dshome-dark] .dshome-dstt-status[data-peak=true]{color:var(--ds-brand-light)}
73
- body[data-dshome-dark] .dshome-dstt-status[data-peak=false]{color:var(--ds-brand-light)}
74
- .dshome-dstt-seg{display:inline-flex;align-items:center;gap:2px;flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);padding:2px}
75
- .dshome-dstt-seg-btn{height:26px;padding:0 12px;border:none;border-radius:999px;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:26px;transition:background .15s ease,color .15s ease}
76
- .dshome-dstt-seg-btn:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)}
77
- .dshome-dstt-seg-btn.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff}
78
- .dshome-dstt-seg-btn:disabled{opacity:.5;cursor:default}
79
- .dshome-dstt-seg-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
80
- `;
81
-
82
- // Locale toggle styling. The two header-hover dependent rules live in the
83
- // `.wSkVaW_root` patch block below, so the toggle stays styled (short
84
- // labels) even if the header class renames.
85
- // The product sets `corner-shape:superellipse(1.5)` on `*` (its
86
- // corner-shape.css), which flattens even a 999px radius into a rounded
87
- // rectangle; `corner-shape:round` restores true round ends here.
88
- // Label bloom: the short 中/E label grows into 中文/EN with an animated
89
- // max-width instead of the old `display` hard swap.
90
- const ACTIONS_CSS = `
91
- .dshome-locale{display:inline-flex;align-items:center;height:28px;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);overflow:hidden;flex:none;corner-shape:round;transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
92
- .dshome-locale:hover{transform:translateY(-1px) scale(1.03);border-color:color-mix(in srgb,var(--ds-brand) 48%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 26%,transparent)}
93
- body[data-dshome-dark] .dshome-locale{border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent)}
94
- body[data-dshome-dark] .dshome-locale:hover{border-color:color-mix(in srgb,var(--ds-brand) 55%,transparent);background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
95
- .dshome-locale button{height:28px;padding:0 11px;border:none;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:28px;transition:background .22s ease,color .22s ease,transform .18s cubic-bezier(.2,.8,.2,1)}
96
- .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep)}
97
- .dshome-locale button:active{transform:scale(.92)}
98
- .dshome-locale button.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;animation:dshome-locale-pop .34s cubic-bezier(.2,.8,.2,1)}
99
- @keyframes dshome-locale-pop{0%{transform:scale(.86)}60%{transform:scale(1.05)}to{transform:none}}
100
- .dshome-locale .dshome-short,.dshome-locale .dshome-full{display:inline-block;overflow:hidden;white-space:nowrap;transition:max-width .3s cubic-bezier(.2,.8,.2,1),opacity .22s ease,transform .3s cubic-bezier(.2,.8,.2,1)}
101
- .dshome-locale .dshome-short{max-width:2.2em;opacity:1;transform:none}
102
- .dshome-locale .dshome-full{max-width:0;opacity:0;transform:translateX(-6px)}
103
- body[data-dshome-dark] .dshome-locale button{color:var(--ds-brand-light)}
104
- body[data-dshome-dark] .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent);color:#fff}
105
- body[data-dshome-dark] .dshome-locale button.dshome-on{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff),var(--ds-brand));color:#fff}
106
- `;
107
-
108
- const SUBAGENT_PANEL_CSS = `
109
- .dshome-sa{position:fixed;right:16px;bottom:16px;z-index:70;width:286px;max-width:calc(100vw - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px;border-radius:16px;border:1px solid rgba(255,255,255,.55);background:linear-gradient(165deg,rgba(255,255,255,.46),rgba(255,255,255,.22));box-shadow:0 12px 40px rgba(13,30,60,.14),inset 0 1px 0 rgba(255,255,255,.85),inset 0 0 0 .5px rgba(255,255,255,.5);-webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);font-family:inherit;transition:opacity .22s ease,transform .22s ease;animation:dshome-sa-in .3s cubic-bezier(.2,.8,.2,1)}
110
- .dshome-sa[hidden]{display:none!important}
111
- .dshome-sa.dshome-sa-leave{opacity:0;transform:translateY(10px);pointer-events:none}
112
- body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(21,42,86,.52),rgba(10,26,58,.3));border-color:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 12px 40px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 .5px color-mix(in srgb,var(--ds-brand) 16%,transparent)}
113
- @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.dshome-sa{background:linear-gradient(165deg,rgba(255,255,255,.84),rgba(255,255,255,.68))}body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(13,30,60,.88),rgba(10,26,58,.78))}}
114
- @keyframes dshome-sa-in{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
115
- .dshome-sa-head{display:flex;flex-direction:row;align-items:center;gap:7px;min-height:26px}
116
- .dshome-sa-pulse{position:relative;flex:none;width:8px;height:8px;border-radius:50%;background:var(--ds-brand);animation:dshome-sa-pulse 1.8s ease-out infinite}
117
- @keyframes dshome-sa-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 45%,transparent)}70%{box-shadow:0 0 0 7px color-mix(in srgb,var(--ds-brand) 0%,transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 0%,transparent)}}
118
- .dshome-sa-title{flex:1;min-width:0;font-size:12.5px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
119
- .dshome-sa-collapse{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:50%;background:rgba(255,255,255,.5);color:var(--ds-brand);cursor:pointer;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 18%,transparent);transition:transform .2s ease,background .15s ease}
120
- .dshome-sa-collapse:hover{background:rgba(255,255,255,.75)}
121
- body[data-dshome-dark] .dshome-sa-collapse{background:color-mix(in srgb,var(--ds-brand) 18%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 25%,transparent)}
122
- body[data-dshome-dark] .dshome-sa-collapse:hover{background:color-mix(in srgb,var(--ds-brand) 30%,transparent)}
123
- .dshome-sa-collapse svg{display:block}
124
- .dshome-sa-collapsed .dshome-sa-collapse{transform:rotate(180deg)}
125
- .dshome-sa-list{display:flex;flex-direction:column;gap:2px;max-height:312px;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;padding:2px 0}
126
- .dshome-sa-collapsed .dshome-sa-list{display:none}
127
- .dshome-sa-item{position:relative;display:flex;flex-direction:row;align-items:flex-start;gap:8px;width:100%;min-width:0;box-sizing:border-box;padding:7px 8px;border:none;border-radius:10px;background:transparent;color:inherit;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease}
128
- .dshome-sa-item:hover{background:linear-gradient(135deg,rgba(255,255,255,.65),rgba(255,255,255,.35));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 14%,transparent)}
129
- body[data-dshome-dark] .dshome-sa-item:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 22%,transparent),color-mix(in srgb,var(--ds-brand) 10%,transparent));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
130
- .dshome-sa-dot{flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-light));animation:dshome-sa-blink 1.2s ease-in-out infinite}
131
- @keyframes dshome-sa-blink{0%,100%{opacity:1}50%{opacity:.35}}
132
- .dshome-sa-stop{flex:none;width:18px;height:18px;margin-top:4px;padding:0;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;pointer-events:none;transition:opacity .15s ease,background .15s ease,color .15s ease}
133
- .dshome-sa-item:hover .dshome-sa-stop,.dshome-sa-item:focus-within .dshome-sa-stop{opacity:1;pointer-events:auto}
134
- .dshome-sa-stop:hover{background:rgba(239,68,68,.12);color:#ef4444}
135
- body[data-dshome-dark] .dshome-sa-stop:hover{background:rgba(255,107,107,.16);color:#ff6b6b}
136
- .dshome-sa-stop:disabled{opacity:.55;cursor:default}
137
- .dshome-sa-stop-busy svg{animation:dshome-sa-blink .8s ease-in-out infinite}
138
- .dshome-sa-stop-denied{background:rgba(239,68,68,.18)!important;color:#ef4444!important}
139
- body[data-dshome-dark] .dshome-sa-stop-denied{background:rgba(255,107,107,.2)!important;color:#ff6b6b!important}
140
- .dshome-sa-sweep{position:absolute;top:0;bottom:0;left:0;width:0;border-radius:10px;background:linear-gradient(90deg,rgba(239,68,68,.14),rgba(239,68,68,.24));pointer-events:none;z-index:0}
141
- body[data-dshome-dark] .dshome-sa-sweep{background:linear-gradient(90deg,rgba(255,107,107,.16),rgba(255,107,107,.28))}
142
- .dshome-sa-item>.dshome-sa-dot,.dshome-sa-item>.dshome-sa-body,.dshome-sa-item>.dshome-sa-stop{position:relative;z-index:1}
143
- .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(239,68,68,.85)}
144
- .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar-fill{display:none}
145
- body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(255,107,107,.8)}
146
- .dshome-sa-item.dshome-sa-stopping .dshome-sa-stop{opacity:0;pointer-events:none}
147
- .dshome-sa-item.dshome-sa-item-exit{opacity:0;transform:translateX(8px);transition:opacity .2s ease,transform .2s ease}
148
- .dshome-sa-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
149
- .dshome-sa-name{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
150
- .dshome-sa-meta{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
151
- .dshome-sa-bar{position:relative;height:3px;border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 12%,transparent);overflow:hidden}
152
- .dshome-sa-bar-fill{position:absolute;inset:0;width:42%;border-radius:999px;background:linear-gradient(90deg,transparent,var(--ds-brand),transparent);animation:dshome-sa-slide 1.5s ease-in-out infinite}
153
- @keyframes dshome-sa-slide{0%{transform:translateX(-110%)}100%{transform:translateX(350%)}}
154
- @media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
155
- `;
156
-
157
- // PATCH_BLOCKS: every rule group that targets product build-time class
158
- // names. `anchor` is the class that must exist in the DOM before the
159
- // block is injected; each block degrades independently when its surface
160
- // renames in a product rebuild.
161
- const PATCH_BLOCKS = [
162
- // Frame / sidebar / details columns.
163
- { anchor: ".pI_x6G_frame", css: `
164
- .pI_x6G_frame,.pI_x6G_sidebarCol,.pI_x6G_detailsCol,.pI_x6G_centerCol{background:transparent!important}
165
- .pI_x6G_sidebarCol{border-right:none!important}
166
- .pI_x6G_detailsCol{border-left:none!important}
167
- ` },
168
- // Sidebar glass root — Apple liquid glass pane: layered translucent
169
- // gradient + blur/saturate/brightness/contrast + specular edges.
170
- { anchor: ".hHd-Xa_root", css: `
171
- .hHd-Xa_root{position:relative;background:transparent!important}
172
- .hHd-Xa_root::before{content:"";position:absolute;inset:0;z-index:-1;background:rgba(255,255,255,.5);-webkit-backdrop-filter:blur(var(--ds-glass-blur));backdrop-filter:blur(var(--ds-glass-blur));border-right:1px solid rgba(0,0,0,.06);pointer-events:none}
173
- body[data-dshome-dark] .hHd-Xa_root::before{background:rgba(10,26,58,.48);border-right:1px solid rgba(255,255,255,.08)}
174
- .hHd-Xa_brand{color:var(--ds-brand);cursor:pointer}
175
- body[data-dshome-dark] .hHd-Xa_brand{color:#fff}
176
- .hHd-Xa_newSession{border:none!important;background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;color:#fff!important;border-radius:999px!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:transform .15s ease,box-shadow .15s ease,background .15s ease}
177
- .hHd-Xa_newSession:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff) 0%,var(--ds-brand) 100%)!important;box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
178
- ` },
179
- // Session / project rows and icon buttons.
180
- { anchor: ".YDXeBa_sessionRow", css: `
181
- .YDXeBa_sessionRow,.YDXeBa_projectRow{background:rgba(255,255,255,.35)!important;border:1px solid transparent;border-radius:10px!important;margin:1px 0;transition:background .15s ease,border-color .15s ease,transform .15s ease}
182
- .YDXeBa_sessionRow:hover,.YDXeBa_projectRow:hover{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transform:translateX(2px)}
183
- .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 16%,transparent),color-mix(in srgb,var(--ds-brand-mid) 12%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 25%,transparent);color:var(--ds-brand-deep)}
184
- body[data-dshome-dark] .YDXeBa_sessionRow,body[data-dshome-dark] .YDXeBa_projectRow{background:rgba(21,42,86,.4)!important}
185
- body[data-dshome-dark] .YDXeBa_sessionRow:hover,body[data-dshome-dark] .YDXeBa_projectRow:hover{background:rgba(30,52,100,.6)!important}
186
- body[data-dshome-dark] .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 28%,transparent),color-mix(in srgb,var(--ds-brand) 20%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);color:#eef2ff}
187
- .YDXeBa_iconButton{transition:background .15s ease,color .15s ease,transform .15s ease,opacity .15s ease}
188
- .YDXeBa_iconButton:hover{transform:scale(1.1)}
189
- ` },
190
- // Chat view: header (incl. locale hover swap), tabs, crumbs, title.
191
- // Hover pill upgraded to a liquid-glass capsule.
192
- // 0.1.5-rc.1 draws the header divider as a real `border-bottom` on
193
- // `.wSkVaW_header` (1.37 used an `::after` pseudo-element), so the
194
- // floating capsule grew a stray hairline under it. Neutralise the
195
- // border here and keep the legacy `::after` hide for older builds.
196
- // The header divider would otherwise be a 0.5px `--dsw-alias-border-l3`
197
- // line spanning exactly this absolutely positioned bar (24px inset).
198
- // `titleRow`/`titleCluster` are flattened with `display:contents`, so
199
- // every direct header child needs an explicit `order`; 0.1.5-rc.1 added
200
- // `headerCorner` (the right-sidebar expand button) and, left at the
201
- // default `order:0`, it tied with the tabs and landed at the far left,
202
- // where the first tab pill overlapped it. It now trails the utilities
203
- // block, i.e. sits right of the 中/EN locale toggle.
204
- { anchor: ".wSkVaW_root", css: `
205
- .wSkVaW_root{position:relative;background:transparent!important}
206
- .wSkVaW_scrollBody{padding-top:56px}
207
- .wSkVaW_viewArea{padding:0 18px}
208
- .wSkVaW_viewArea{animation:dshome-view-enter .45s ease}
209
- .wSkVaW_header{position:absolute;top:8px;left:24px;right:24px;z-index:5;min-height:36px;padding:4px 12px;display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-start;border:0!important;border-radius:0;background:transparent!important;transition:border-radius .3s ease}
210
- .wSkVaW_header::before{content:"";position:absolute;inset:0;z-index:-1;background:transparent;border:1px solid transparent;border-radius:inherit;-webkit-backdrop-filter:none;backdrop-filter:none;transition:background .3s ease,border-color .3s ease,box-shadow .3s ease}
211
- .wSkVaW_header:hover{border-radius:999px}
212
- .wSkVaW_titleRow{display:contents}
213
- .wSkVaW_titleCluster{display:contents}
214
- .wSkVaW_crumbs{order:2;flex:none;min-width:0}
215
- .wSkVaW_tabs{order:0;flex:none;align-items:center;display:none;gap:8px;margin:0;padding:0}
216
- .wSkVaW_headerActions{order:1;flex:none;align-items:center;display:flex;gap:8px}
217
- .wSkVaW_headerUtilities{order:3;flex:none;align-items:center;display:flex;gap:8px;margin-left:auto}
218
- .wSkVaW_headerCorner{order:4;flex:none;align-items:center;display:flex;margin-left:0;margin-right:0}
219
- .wSkVaW_header.dshome-swap .wSkVaW_tabs{display:flex;animation:dshome-tab-in .22s ease}
220
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_tabs{display:flex!important}
221
- .wSkVaW_header:hover::before{background:rgba(255,255,255,.45);border-color:rgba(0,0,0,.06);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 4px 16px rgba(0,0,0,.05)}
222
- body[data-dshome-dark] .wSkVaW_header:hover::before{background:rgba(13,30,60,.45);border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.3)}
223
- .wSkVaW_header:after{display:none!important}
224
- .SVAs4q_label{background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent);border-radius:999px!important;padding:0 10px!important;height:28px;line-height:26px;color:var(--ds-brand)!important}
225
- body[data-dshome-dark] .SVAs4q_label{color:var(--ds-brand-light)!important}
226
- .wSkVaW_tab{border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);border-radius:999px;corner-shape:round;padding:5px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:16px;font-weight:500;transition:color .22s ease,background .22s ease,border-color .22s ease,box-shadow .24s ease,transform .24s cubic-bezier(.2,.8,.2,1)}
227
- .wSkVaW_tab:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);border-color:color-mix(in srgb,var(--ds-brand) 42%,transparent);box-shadow:0 4px 12px color-mix(in srgb,var(--ds-brand) 22%,transparent);transform:translateY(-1px)}
228
- .wSkVaW_tab:active{transform:translateY(0) scale(.96)}
229
- .wSkVaW_tab::after{display:none!important}
230
- .wSkVaW_tabActive{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;border-color:transparent;box-shadow:0 3px 10px color-mix(in srgb,var(--ds-brand) 26%,transparent);animation:dshome-tab-pop .34s cubic-bezier(.2,.8,.2,1)}
231
- .wSkVaW_tabActive:hover{color:#fff}
232
- body[data-dshome-dark] .wSkVaW_tab{border-color:color-mix(in srgb,var(--ds-brand) 32%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent)}
233
- body[data-dshome-dark] .wSkVaW_tab:hover{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
234
- body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb,var(--ds-brand) 40%,transparent)}
235
- @keyframes dshome-tab-pop{0%{transform:translateY(-1px) scale(.88)}60%{transform:translateY(-1px) scale(1.05)}to{transform:none}}
236
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover{border-radius:0}
237
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover::before{background:transparent;border-color:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
238
- .wSkVaW_composerSeat{background:transparent!important}
239
- .wSkVaW_header:hover .dshome-locale .dshome-short{max-width:0;opacity:0;transform:translateX(6px)}
240
- .wSkVaW_header:hover .dshome-locale .dshome-full{max-width:3em;opacity:1;transform:none}
241
- /* Header chips: the product's global corner-shape:superellipse(1.5) squares off
242
- even pill radii, so every control in the bar is restored to a true round end
243
- (and the session-title crumb, 12px on a 28px box, becomes a real pill). */
244
- .wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header .nL4_yW_sessionLogButton,.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
245
- .wSkVaW_header .wSkVaW_crumb{border-radius:999px}
246
- /* Open-in-app split button: hover lift with a brand tint, and the chevron turns
247
- over while its menu is open. */
248
- .wSkVaW_header .CAgGvG_split{transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
249
- .wSkVaW_header .CAgGvG_split:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ds-brand) 40%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
250
- .wSkVaW_header .CAgGvG_split:active{transform:translateY(0) scale(.97)}
251
- .wSkVaW_header .CAgGvG_main,.wSkVaW_header .CAgGvG_chevron{transition:background .22s ease,color .22s ease}
252
- .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,.wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand-deep)}
253
- .wSkVaW_header .CAgGvG_chevron svg{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
254
- .wSkVaW_header .CAgGvG_chevron[aria-expanded=true] svg{transform:rotate(180deg)}
255
- body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover{border-color:color-mix(in srgb,var(--ds-brand) 52%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 30%,transparent)}
256
- body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);color:#fff}
257
- ` },
258
- // Session-log button (hover expand).
259
- { anchor: ".nL4_yW_sessionLogButton", css: `
260
- .nL4_yW_sessionLogButton{min-width:0!important;width:28px;height:28px;padding:0!important;border-radius:50%!important;border:none!important;background:color-mix(in srgb,var(--ds-brand) 8%,transparent)!important;color:var(--ds-brand)!important;overflow:hidden;transition:width .2s ease,padding .2s ease,border-radius .2s ease}
261
- .nL4_yW_sessionLogButton span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
262
- .wSkVaW_header:hover .nL4_yW_sessionLogButton{width:auto;padding:0 12px!important;border-radius:18px!important}
263
- .wSkVaW_header:hover .nL4_yW_sessionLogButton span{max-width:90px;opacity:1}
264
- body[data-dshome-dark] .nL4_yW_sessionLogButton{color:var(--ds-brand-light)!important}
265
- ` },
266
- // Glass cards with spinning gradient border — upgraded to liquid glass:
267
- // specular top edge, saturated blur, brighter inner rim.
268
- { anchor: ".uV2eYG_card", css: `
269
- .uV2eYG_card{position:relative;background:rgba(255,255,255,.42)!important;border:1.5px solid rgba(255,255,255,.55)!important;box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8);border-radius:22px!important}
270
- body[data-dshome-dark] .uV2eYG_card{background:rgba(13,30,60,.55)!important;border:1.5px solid rgba(255,255,255,.16)!important;box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
271
- .uV2eYG_card::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:22px;-webkit-backdrop-filter:blur(28px);backdrop-filter:blur(28px);pointer-events:none}
272
- .uV2eYG_card::before{content:"";position:absolute;inset:-1px;border-radius:inherit;padding:1px;background:conic-gradient(from var(--dsh-border-angle),color-mix(in srgb,var(--ds-brand) 0%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand-light) 20%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand) 0%,transparent));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;pointer-events:none;animation:dsh-border-spin 6s linear infinite;z-index:0}
273
- body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(--dsh-border-angle),rgba(255,255,255,0),color-mix(in srgb,var(--ds-brand) 55%,transparent),color-mix(in srgb,var(--ds-brand-light) 25%,transparent),color-mix(in srgb,var(--ds-brand) 55%,transparent),rgba(255,255,255,0))}
274
- .uV2eYG_card>*{position:relative;z-index:1}
275
- .uV2eYG_primary{background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;border:none!important;color:#fff!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:box-shadow .15s ease,transform .15s ease}
276
- .uV2eYG_primary:hover{box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
277
- ` },
278
- // Empty-state headline + preview badge.
279
- { anchor: ".pXSMma_headlineText", css: `
280
- .pXSMma_headlineText{font-size:34px;line-height:42px;font-weight:700;letter-spacing:-.01em;background:linear-gradient(120deg,#152443 0%,var(--ds-brand-deep) 55%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
281
- body[data-dshome-dark] .pXSMma_headlineText{background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
282
- .pXSMma_previewBadge{border-radius:999px!important;background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent)!important;color:var(--ds-brand)!important}
283
- .pXSMma_fish{color:var(--ds-brand)!important}
284
- ` },
285
- // Chat bubbles — liquid glass panes.
286
- { anchor: ".gdEzaW_bubble", css: `
287
- .gdEzaW_bubble{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.05);box-shadow:0 4px 16px rgba(13,30,60,.06)}
288
- body[data-dshome-dark] .gdEzaW_bubble{background:rgba(21,42,86,.65)!important;border:1px solid rgba(255,255,255,.1);box-shadow:0 4px 16px rgba(0,0,0,.25)}
289
- ` },
290
- // Trajectory view bar.
291
- { anchor: ".fV0t5q_root", css: `
292
- .fV0t5q_root{background:rgba(255,255,255,.45)!important;border-bottom:1px solid rgba(0,0,0,.06)!important}
293
- body[data-dshome-dark] .fV0t5q_root{background:rgba(13,30,60,.45)!important;border-bottom-color:rgba(255,255,255,.08)!important}
294
- ` },
295
- // Timeline root/plot/span.
296
- { anchor: "._1p9O6q_root", css: `
297
- ._1p9O6q_root{background:rgba(255,255,255,.3)!important;border-bottom:1px solid rgba(0,0,0,.05)!important}
298
- ._1p9O6q_plot{background:transparent!important}
299
- body[data-dshome-dark] ._1p9O6q_root{background:rgba(13,30,60,.3)!important}
300
- ._1p9O6q_span[data-timeline-span=assistant]{background:var(--ds-brand)!important}
301
- ._1p9O6q_span[data-timeline-span=user]{background:var(--ds-brand-light)!important}
302
- ` },
303
- // Progress panel entrance.
304
- { anchor: ".Nqubda_panel", css: `
305
- .Nqubda_panel{animation:dshome-pop-in .25s cubic-bezier(.2,.8,.2,1)}
306
- ` },
307
- // Settings overlay / modal — near-opaque glass with specular edge.
308
- { anchor: ".VOzbGW_panel", css: `
309
- .VOzbGW_mask{animation:dshome-fade-in .2s ease}
310
- .VOzbGW_panel{background:rgba(255,255,255,.97)!important;animation:dshome-modal-in .4s cubic-bezier(.2,.8,.2,1)!important}
311
- body[data-dshome-dark] .VOzbGW_panel{background:rgba(13,30,60,.96)!important}
312
- .VOzbGW_overlay{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;inset:0!important}
313
- ` }
314
- ];
315
-
316
- /** Append a style tag; returns a disposer that removes it. */
317
- function injectStyle(css, label) {
318
- const tag = document.createElement('style');
319
- tag.dataset.plugin = PLUGIN_ID;
320
- tag.dataset.pluginCss = PLUGIN_ID + '/' + label;
321
- tag.textContent = css;
322
- document.head.appendChild(tag);
323
- return () => tag.remove();
324
- }
325
-
326
- /**
327
- * Inject `css` only while `anchor` matches the DOM. If the anchor is
328
- * absent at call time, wait up to 30s for it (covers shells that render
329
- * after the plugin activates); if it never appears, skip the block and log
330
- * one diagnostic line instead of shipping dead CSS against a renamed
331
- * product surface. Returns a disposer for the whole install.
332
- */
333
- function installGuarded(anchor, css) {
334
- let dispose = null;
335
- const applyNow = () => {
336
- if (dispose !== null) return true;
337
- if (document.querySelector(anchor) === null) return false;
338
- dispose = injectStyle(css, 'patch' + anchor);
339
- return true;
340
- };
341
- if (applyNow()) {
342
- return () => { if (dispose !== null) dispose(); };
343
- }
344
- let timer = 0;
345
- let mo = new MutationObserver(() => {
346
- if (!applyNow()) return;
347
- mo.disconnect();
348
- clearTimeout(timer);
349
- });
350
- try {
351
- mo.observe(document.body, { childList: true, subtree: true });
352
- } catch (error) {
353
- mo = null;
354
- }
355
- timer = setTimeout(() => {
356
- if (mo !== null) mo.disconnect();
357
- if (dispose === null) {
358
- console.info('[' + PLUGIN_ID + '] patch skipped: ' + anchor + ' not found in DOM (product surface renamed?)');
359
- }
360
- }, 30000);
361
- return () => {
362
- if (mo !== null) mo.disconnect();
363
- clearTimeout(timer);
364
- if (dispose !== null) dispose();
365
- };
366
- }
367
-
368
- /**
369
- * Own the light/dark marker: reflect the theme service's active color
370
- * scheme onto our own `data-dshome-dark` attribute, so dark styling never
371
- * depends on the product's attribute name. Falls back to mirroring the
372
- * product attribute only when the theme service or the `theme/change`
373
- * subscription is unavailable.
374
- */
375
- function createDarkSync(ctx, theme) {
376
- const apply = () => {
377
- let dark = false;
378
- if (theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
379
- try {
380
- const snap = theme.getTheme();
381
- dark = snap !== undefined && snap !== null &&
382
- snap.active !== undefined && snap.active !== null &&
383
- snap.active.colorScheme === 'dark';
384
- } catch (error) {
385
- dark = false;
386
- }
387
- }
388
- if (!dark) dark = document.body.hasAttribute('data-ds-dark-theme');
389
- if (dark) document.body.setAttribute('data-dshome-dark', '');
390
- else document.body.removeAttribute('data-dshome-dark');
391
- };
392
- apply();
393
- let unsubscribe = null;
394
- try {
395
- unsubscribe = ctx.on('theme/change', apply);
396
- } catch (error) {
397
- unsubscribe = null;
398
- }
399
- let mirror = null;
400
- if (unsubscribe === null || theme === undefined || theme === null || typeof theme.getTheme !== 'function') {
401
- try {
402
- mirror = new MutationObserver(apply);
403
- mirror.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] });
404
- } catch (error) {
405
- mirror = null;
406
- }
407
- }
408
- return () => {
409
- if (unsubscribe !== null) unsubscribe();
410
- if (mirror !== null) mirror.disconnect();
411
- };
412
- }
413
-
414
- function startParticles() {
415
- const canvas = document.createElement('canvas');
416
- canvas.setAttribute('data-dsh-deepseek-canvas', '');
417
- const style = canvas.style;
418
- style.position = 'fixed';
419
- style.top = '0';
420
- style.left = '0';
421
- style.width = '100%';
422
- style.height = '100%';
423
- style.zIndex = '-1';
424
- style.pointerEvents = 'none';
425
- style.opacity = '0.8';
426
- document.body.appendChild(canvas);
427
-
428
- const g = canvas.getContext('2d');
429
- const isDark = () => document.body.hasAttribute('data-dshome-dark');
430
- const activeColor = () => {
431
- const c = document.body.getAttribute('data-dshome-color');
432
- return c === 'red' || c === 'blue' ? c : 'green';
433
- };
434
- let running = true;
435
- let rafId = 0;
436
- let W = 0;
437
- let H = 0;
438
- let dpr = 1;
439
- const COUNT = 72;
440
- const parts = [];
441
-
442
- function palette() {
443
- const dark = isDark();
444
- const color = activeColor();
445
- if (color === 'red') {
446
- return dark ? ['#ff4d4f', '#f5222d', '#ff7875', '#ffa39e', '#ffffff'] : ['#f5222d', '#ff7875', '#ff4d4f', '#ffffff'];
447
- }
448
- if (color === 'blue') {
449
- return dark ? ['#5d79ff', '#4d6bfe', '#8fa8ff', '#ffffff', '#9db9ff'] : ['#4d6bfe', '#73a3d2', '#9db9ff', '#ffffff'];
450
- }
451
- return dark ? ['#34d399', '#059669', '#6ee7b7', '#a7f3d0', '#ffffff'] : ['#10b981', '#6ee7b7', '#34d399', '#ffffff'];
452
- }
453
-
454
- function linkBase() {
455
- const dark = isDark();
456
- const color = activeColor();
457
- if (color === 'red') return dark ? 'rgba(255,77,79,' : 'rgba(245,34,45,';
458
- if (color === 'blue') return dark ? 'rgba(93,121,255,' : 'rgba(77,107,254,';
459
- return dark ? 'rgba(52,211,153,' : 'rgba(16,185,129,';
460
- }
461
-
462
- function resize() {
463
- dpr = Math.min(window.devicePixelRatio || 1, 2);
464
- W = window.innerWidth;
465
- H = window.innerHeight;
466
- canvas.width = Math.round(W * dpr);
467
- canvas.height = Math.round(H * dpr);
468
- g.setTransform(dpr, 0, 0, dpr, 0, 0);
469
- }
470
-
471
- function make() {
472
- const colors = palette();
473
- return {
474
- x: Math.random() * W,
475
- y: Math.random() * H,
476
- vx: (Math.random() - 0.5) * 0.28,
477
- vy: (Math.random() - 0.5) * 0.28,
478
- r: Math.random() * 1.7 + 0.6,
479
- a: Math.random() * 0.55 + 0.15,
480
- c: colors[(Math.random() * colors.length) | 0]
481
- };
482
- }
483
-
484
- function step() {
485
- if (!running) return;
486
- g.clearRect(0, 0, W, H);
487
- const base = linkBase();
488
- for (let i = 0; i < parts.length; i++) {
489
- for (let j = i + 1; j < parts.length; j++) {
490
- const a = parts[i];
491
- const b = parts[j];
492
- const dx = a.x - b.x;
493
- const dy = a.y - b.y;
494
- const d2 = dx * dx + dy * dy;
495
- if (d2 < 14400) {
496
- const alpha = (1 - Math.sqrt(d2) / 120) * 0.16;
497
- g.strokeStyle = base + alpha + ')';
498
- g.lineWidth = 1;
499
- g.beginPath();
500
- g.moveTo(a.x, a.y);
501
- g.lineTo(b.x, b.y);
502
- g.stroke();
503
- }
504
- }
505
- }
506
- for (const p of parts) {
507
- p.x += p.vx;
508
- p.y += p.vy;
509
- if (p.x < -20) p.x = W + 20;
510
- else if (p.x > W + 20) p.x = -20;
511
- if (p.y < -20) p.y = H + 20;
512
- else if (p.y > H + 20) p.y = -20;
513
- g.globalAlpha = p.a;
514
- g.fillStyle = p.c;
515
- g.beginPath();
516
- g.arc(p.x, p.y, p.r, 0, Math.PI * 2);
517
- g.fill();
518
- }
519
- g.globalAlpha = 1;
520
- rafId = requestAnimationFrame(step);
521
- }
522
-
523
- const refreshColors = () => {
524
- const colors = palette();
525
- for (const p of parts) p.c = colors[(Math.random() * colors.length) | 0];
526
- };
527
- const unsubscribeColor = subscribeColor(refreshColors);
528
-
529
- window.addEventListener('resize', resize);
530
- resize();
531
- for (let i = 0; i < COUNT; i++) parts.push(make());
532
- rafId = requestAnimationFrame(step);
533
-
534
- return () => {
535
- running = false;
536
- cancelAnimationFrame(rafId);
537
- window.removeEventListener('resize', resize);
538
- unsubscribeColor();
539
- canvas.remove();
540
- };
541
- }
542
-
543
- function watchSettingsSection() {
544
- let lastChild = null;
545
- const mo = new MutationObserver(() => {
546
- const options = document.querySelector('.VOzbGW_options');
547
- if (options === null) return;
548
- const child = options.firstElementChild;
549
- if (child !== lastChild) {
550
- lastChild = child;
551
- if (child !== null) {
552
- child.style.animation = 'none';
553
- void child.offsetWidth;
554
- child.style.animation = 'dshome-fade-in .25s ease';
555
- }
556
- }
557
- });
558
- mo.observe(document.body, { subtree: true, childList: true });
559
- return () => mo.disconnect();
560
- }
561
-
562
- function watchBrandLink() {
563
- const onClick = (event) => {
564
- const brand = event.target && event.target.closest ? event.target.closest('.hHd-Xa_brand') : null;
565
- if (brand === null) return;
566
- event.preventDefault();
567
- event.stopPropagation();
568
- window.open('https://www.deepseek.com/', '_blank', 'noopener,noreferrer');
569
- };
570
- document.addEventListener('click', onClick, true);
571
- return () => document.removeEventListener('click', onClick, true);
572
- }
573
-
574
- function watchHeaderInteraction() {
575
- const EASE = 'cubic-bezier(.2,.8,.2,1)';
576
- const DUR = '.3s';
577
- const timers = [];
578
-
579
- const headerOf = (node) => (node && node.closest ? node.closest('.wSkVaW_header') : null);
580
- const isTrajectory = (header) => {
581
- const root = header.closest('.wSkVaW_root');
582
- return root !== null && root.querySelector('.fV0t5q_root') !== null;
583
- };
584
-
585
- const enter = (header) => {
586
- const actions = header.querySelector('.wSkVaW_headerActions');
587
- const crumbs = header.querySelector('.wSkVaW_crumbs');
588
- if (crumbs === null) return;
589
-
590
- const actionsFirst = actions ? actions.getBoundingClientRect() : null;
591
- const crumbsFirst = crumbs.getBoundingClientRect();
592
-
593
- header.classList.add('dshome-swap');
594
-
595
- const actionsFlow = actions ? actions.getBoundingClientRect() : null;
596
- const crumbsFlow = crumbs.getBoundingClientRect();
597
-
598
- if (actions && actionsFirst && actionsFlow) {
599
- const dx = actionsFirst.left - actionsFlow.left;
600
- const dy = actionsFirst.top - actionsFlow.top;
601
- actions.style.transition = 'none';
602
- actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
603
- }
604
-
605
- const cdx = crumbsFirst.left - crumbsFlow.left;
606
- const cdy = crumbsFirst.top - crumbsFlow.top;
607
- crumbs.style.transition = 'none';
608
- crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
609
-
610
- void header.offsetWidth;
611
-
612
- if (actions) {
613
- actions.style.transition = 'transform ' + DUR + ' ' + EASE;
614
- actions.style.transform = 'translate(0,0)';
615
- }
616
- crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
617
- crumbs.style.transform = 'translate(0,0)';
618
-
619
- const timer = setTimeout(() => {
620
- if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
621
- crumbs.style.transition = '';
622
- crumbs.style.transform = '';
623
- }, 320);
624
- timers.push(timer);
625
- };
626
-
627
- const leave = (header) => {
628
- const actions = header.querySelector('.wSkVaW_headerActions');
629
- const crumbs = header.querySelector('.wSkVaW_crumbs');
630
-
631
- const actionsHover = actions ? actions.getBoundingClientRect() : null;
632
- const crumbsHover = crumbs ? crumbs.getBoundingClientRect() : null;
633
-
634
- header.classList.remove('dshome-swap');
635
-
636
- if (actions && actionsHover) {
637
- const n = actions.getBoundingClientRect();
638
- const dx = actionsHover.left - n.left;
639
- const dy = actionsHover.top - n.top;
640
- actions.style.transition = 'none';
641
- actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
642
- }
643
-
644
- if (crumbs && crumbsHover) {
645
- const n = crumbs.getBoundingClientRect();
646
- const cdx = crumbsHover.left - n.left;
647
- const cdy = crumbsHover.top - n.top;
648
- crumbs.style.transition = 'none';
649
- crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
650
- }
651
-
652
- void header.offsetWidth;
653
-
654
- if (actions) {
655
- actions.style.transition = 'transform ' + DUR + ' ' + EASE;
656
- actions.style.transform = 'translate(0,0)';
657
- }
658
- if (crumbs) {
659
- crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
660
- crumbs.style.transform = 'translate(0,0)';
661
- }
662
-
663
- const timer = setTimeout(() => {
664
- if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
665
- if (crumbs) { crumbs.style.transition = ''; crumbs.style.transform = ''; }
666
- }, 320);
667
- timers.push(timer);
668
- };
669
-
670
- const over = (event) => {
671
- const header = headerOf(event.target);
672
- if (header === null || isTrajectory(header)) return;
673
- if (header.classList.contains('dshome-swap')) return;
674
- const from = event.relatedTarget;
675
- if (from && header.contains(from)) return;
676
- enter(header);
677
- };
678
-
679
- const out = (event) => {
680
- const header = headerOf(event.target);
681
- if (header === null) return;
682
- if (!header.classList.contains('dshome-swap')) return;
683
- const to = event.relatedTarget;
684
- if (to && header.contains(to)) return;
685
- leave(header);
686
- };
687
-
688
- document.addEventListener('mouseover', over);
689
- document.addEventListener('mouseout', out);
690
-
691
- return () => {
692
- document.removeEventListener('mouseover', over);
693
- document.removeEventListener('mouseout', out);
694
- timers.forEach((t) => clearTimeout(t));
695
- document.querySelectorAll('.wSkVaW_header.dshome-swap').forEach((h) => {
696
- h.classList.remove('dshome-swap');
697
- const a = h.querySelector('.wSkVaW_headerActions');
698
- const c = h.querySelector('.wSkVaW_crumbs');
699
- if (a) { a.style.transition = ''; a.style.transform = ''; }
700
- if (c) { c.style.transition = ''; c.style.transform = ''; }
701
- });
702
- };
703
- }
704
-
705
- /**
706
- * Add an "Open workspace" entry to the folder row's (workspace row) three-dot
707
- * menu. The workspace browser renders its row menus internally with no plugin
708
- * slot, so the theme patches the popup DOM: it tracks the ellipsis anchor of a
709
- * project row, and when the portaled [role=menu] appears it inserts an item
710
- * that resolves the row's workspace (title match against the workspaces
711
- * store, DOM-order fallback) and asks the host half (connection RPC) to open
712
- * the workspace's directory with the OS file manager in the foreground.
713
- * The private host channel is the primary reveal path; `workspaces.openPath`
714
- * is used only when it still exists (dsh ≤0.1.1), and a failed reveal is
715
- * reported on the console instead of failing silently.
716
- */
717
- function watchWorkspaceMenu(ctx) {
718
- let workspaces;
719
- try {
720
- workspaces = ctx.get('workspaces');
721
- } catch (error) {
722
- workspaces = undefined;
723
- }
724
- let connection;
725
- try {
726
- connection = ctx.get('connection');
727
- } catch (error) {
728
- connection = undefined;
729
- }
730
- if (workspaces === undefined || workspaces === null || workspaces.list === undefined) return () => {};
731
-
732
- const ROW = '.YDXeBa_projectRow';
733
- const ACTIONS = '.YDXeBa_rowActions';
734
- const LABEL = '.YDXeBa_title';
735
- const CLS = 'dshome-ws-open';
736
- const FOLDER_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
737
-
738
- let label = '打开工作区';
739
- try {
740
- const locale = ctx.get('locale');
741
- if (locale !== undefined) {
742
- const active = locale.getLocale().active;
743
- if (active !== 'zh' && active !== 'zh-CN' && active !== 'zh-Hans') label = 'Open workspace';
744
- }
745
- } catch (error) {}
746
-
747
- let anchorRow = null;
748
-
749
- const resolveWorkspace = (row) => {
750
- try {
751
- const items = workspaces.list.getSnapshot().items;
752
- if (!Array.isArray(items)) return undefined;
753
- const titleEl = row.querySelector(LABEL);
754
- const labelText = titleEl === null ? '' : (titleEl.textContent || '').trim();
755
- if (labelText === '') return undefined;
756
- const byTitle = items.filter((w) => w !== null && w !== undefined && w.title === labelText);
757
- if (byTitle.length === 1) return byTitle[0];
758
- const rows = Array.from(document.querySelectorAll(ROW));
759
- const index = rows.indexOf(row);
760
- if (index === -1) return undefined;
761
- let ordinal = 0;
762
- for (let i = 0; i < index; i++) {
763
- const t = rows[i].querySelector(LABEL);
764
- const l = t === null ? '' : (t.textContent || '').trim();
765
- if (l !== '' && l !== 'Ungrouped') ordinal += 1;
766
- }
767
- const candidate = items[ordinal];
768
- return candidate === undefined || candidate === null ? undefined : candidate;
769
- } catch (error) {
770
- return undefined;
771
- }
772
- };
773
-
774
- const track = (target) => {
775
- anchorRow = null;
776
- if (target === null || typeof target.closest !== 'function') return;
777
- const button = target.closest('button');
778
- if (button === null) return;
779
- const actions = button.closest(ACTIONS);
780
- if (actions === null) return;
781
- const row = actions.closest(ROW);
782
- if (row === null) return;
783
- if (button !== actions.querySelector('button')) return;
784
- anchorRow = row;
785
- };
786
-
787
- const onClickCapture = (event) => track(event.target);
788
- const onFocusIn = (event) => track(event.target);
789
- document.addEventListener('click', onClickCapture, true);
790
- document.addEventListener('focusin', onFocusIn, true);
791
-
792
- const injectItem = (menu) => {
793
- if (menu === null || menu.querySelector('.' + CLS) !== null) return;
794
- const viewport = menu.querySelector('[role="presentation"]');
795
- if (viewport === null) return;
796
- const row = anchorRow;
797
- if (row === null || !document.contains(row)) return;
798
- const workspace = resolveWorkspace(row);
799
- if (workspace === undefined) return;
800
-
801
- const wrap = document.createElement('div');
802
- wrap.className = CLS + '-wrap';
803
- const item = document.createElement('button');
804
- item.type = 'button';
805
- item.className = CLS;
806
- item.setAttribute('role', 'menuitem');
807
- item.title = label;
808
- item.setAttribute('aria-label', label);
809
- const icon = document.createElement('span');
810
- icon.className = CLS + '-icon';
811
- icon.innerHTML = FOLDER_SVG;
812
- const text = document.createElement('span');
813
- text.className = CLS + '-label';
814
- text.textContent = label;
815
- item.appendChild(icon);
816
- item.appendChild(text);
817
- item.addEventListener('click', (event) => {
818
- event.preventDefault();
819
- event.stopPropagation();
820
- const path = workspace.path;
821
- // The host bridge is a same-origin webServer route (see bridgeCall).
822
- const fallback = (reason) => {
823
- if (typeof workspaces.openPath === 'function') {
824
- try {
825
- workspaces.openPath(path);
826
- return;
827
- } catch (error) {
828
- console.warn('[deepseek-style-theme] workspaces.openPath failed:', error);
829
- }
830
- }
831
- console.warn(
832
- '[deepseek-style-theme] 打开工作区失败(' + reason + '):' +
833
- '私有 RPC 通道不可用,且当前 dsh 版本已移除 workspaces.openPath。' +
834
- '请确认插件为 1.38.4+ 并重启 dsh web(旧版宿主端因缺少 webServer 注入无法注册通道)。'
835
- );
836
- };
837
- Promise.resolve(bridgeCall('dshome/explorer.open', { path: path }))
838
- .then((result) => {
839
- if (result !== undefined && result !== null && result.ok === true) return;
840
- const detail = result !== undefined && result !== null && result.error !== undefined
841
- ? String(result.error.message)
842
- : 'no result';
843
- fallback('bridge returned ' + detail);
844
- })
845
- .catch((error) => {
846
- fallback('bridge call threw: ' + (error instanceof Error ? error.message : String(error)));
847
- });
848
- try {
849
- document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
850
- } catch (error) {}
851
- });
852
- wrap.appendChild(item);
853
- viewport.insertBefore(wrap, viewport.firstChild);
854
- };
855
-
856
- const observer = new MutationObserver((records) => {
857
- for (const record of records) {
858
- for (const node of record.addedNodes) {
859
- if (!(node instanceof Element)) continue;
860
- if (node.matches('[role="menu"]')) {
861
- injectItem(node);
862
- continue;
863
- }
864
- node.querySelectorAll('[role="menu"]').forEach(injectItem);
865
- }
866
- }
867
- });
868
- observer.observe(document.body, { childList: true, subtree: true });
869
-
870
- return () => {
871
- observer.disconnect();
872
- document.removeEventListener('click', onClickCapture, true);
873
- document.removeEventListener('focusin', onFocusIn, true);
874
- };
875
- }
876
-
877
- /**
878
- * Action menu for the product's delivered/changed files
879
- * (`@deepseek-ai/dsh-client-ui-deliverables`), which renders two separate
880
- * surfaces:
881
- *
882
- * - `[data-produced-files-row]` — the "本轮文件改动" chips, one
883
- * `button[title=absolutePath]` per changed path, whose only own action
884
- * is the product's preview opener;
885
- * - `[data-presented-file]` — the declared-file cards, whose overlay button
886
- * previews and whose chevron already offers default-app open and reveal.
887
- *
888
- * Both now open this menu: the changed-file chips on a plain left click
889
- * (clicking one previously looked like nothing happened) and both surfaces
890
- * on right click. The product's own action is preserved as a menu item, so
891
- * hijacking the chip click trades nothing away. "Copy path" does not exist
892
- * anywhere in the product — the absolute path is otherwise only reachable by
893
- * reading a `title` tooltip by hand — so it is also added to the product's
894
- * own chevron menu.
895
- *
896
- * Both anchors are stable product attributes rather than build-hashed class
897
- * names, and each surface already carries the absolute path in its `title`
898
- * (a card via the product's own `resolveWorkspacePath`), so this patch never
899
- * re-derives a workspace.
900
- */
901
- function watchFileCardMenu(ctx) {
902
- const CARD = '[data-presented-file]';
903
- // The product renders changed files through a *different* component:
904
- // ProducedFiles draws one `button[title=absolutePath]` per path inside
905
- // `[data-produced-files-row]`. Those chips are not
906
- // `[data-presented-file]` cards at all, so they need their own anchor.
907
- const PRODUCED = '[data-produced-files-row] button[title]';
908
- const MENU = '[role="menu"]';
909
- const CLS = 'dshome-file-menu';
910
- const ITEM_CLS = CLS + '-item';
911
- const TOAST_CLS = CLS + '-toast';
912
- const COPY_CLS = 'dshome-file-copy';
913
- const ICON_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>';
914
- const ICON_REVEAL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
915
- const ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
916
- const ICON_PREVIEW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>';
917
- const CSS = `
918
- .${CLS}{position:fixed;z-index:2147483000;min-width:198px;padding:6px;border-radius:14px;background:rgba(255,255,255,.94);border:.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));box-shadow:0 18px 44px rgba(15,30,60,.2);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);animation:dshome-menu-pop .16s cubic-bezier(.2,.8,.2,1);transform-origin:top left}
919
- body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 44px rgba(0,0,0,.5)}
920
- .${ITEM_CLS},.${COPY_CLS}{display:flex;align-items:center;gap:9px;box-sizing:border-box;width:100%;padding:8px 10px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#152443);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;transition:background-color .12s ease,transform .12s ease}
921
- .${ITEM_CLS}:hover,.${COPY_CLS}:hover{background:color-mix(in srgb,var(--ds-brand,#4d6bfe) 13%,transparent);transform:translateX(1px)}
922
- .${ITEM_CLS}:focus-visible,.${COPY_CLS}:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}
923
- .${ITEM_CLS} svg,.${COPY_CLS} svg{flex:none;width:15px;height:15px;opacity:.72}
924
- .${ITEM_CLS}>span,.${COPY_CLS}>span{display:inline-flex;align-items:center;min-width:0}
925
- .${CLS}-sep{height:1px;margin:5px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
926
- .${TOAST_CLS}{position:fixed;z-index:2147483001;left:50%;bottom:56px;transform:translateX(-50%);padding:8px 15px;border-radius:999px;background:rgba(20,36,70,.93);color:#fff;font-size:12px;line-height:18px;pointer-events:none;animation:dshome-fade-in .18s ease}
927
- @keyframes dshome-menu-pop{from{opacity:0;transform:scale(.96) translateY(-4px)}to{opacity:1;transform:none}}
928
- `;
929
-
930
- const isZh = () => {
931
- try {
932
- const locale = ctx.get('locale');
933
- if (locale === undefined || locale === null) return true;
934
- const active = locale.getLocale().active;
935
- return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
936
- } catch (error) {
937
- return true;
938
- }
939
- };
940
- const labels = () => (isZh() ? {
941
- open: '用默认应用打开',
942
- reveal: '打开所在文件夹',
943
- copy: '复制路径',
944
- preview: '在侧边栏预览',
945
- previewProduced: '打开预览',
946
- copied: '已复制路径',
947
- copyDenied: '浏览器拒绝了剪贴板访问,请手动复制',
948
- failed: '操作失败(宿主端未响应)'
949
- } : {
950
- open: 'Open with default app',
951
- reveal: 'Open containing folder',
952
- copy: 'Copy path',
953
- preview: 'Preview in sidebar',
954
- previewProduced: 'Open preview',
955
- copied: 'Path copied',
956
- copyDenied: 'Clipboard access was denied — copy it manually',
957
- failed: 'Action failed (no response from the host)'
958
- });
959
-
960
- /** The absolute path the product already resolved onto a card. */
961
- const pathOf = (card) => {
962
- const overlay = card.querySelector('button[title]');
963
- if (overlay === null) return '';
964
- const value = overlay.getAttribute('title');
965
- return typeof value === 'string' ? value.trim() : '';
966
- };
967
-
968
- /**
969
- * Resolve a click target to the file it represents. A changed-file chip
970
- * is both the anchor and the button whose own click opens the product's
971
- * preview; a presented card carries that button as an overlay. Returns
972
- * null when the target is neither.
973
- */
974
- const surfaceOf = (target) => {
975
- if (target === null || typeof target.closest !== 'function') return null;
976
- const chip = target.closest(PRODUCED);
977
- if (chip !== null) {
978
- const chipPath = chip.getAttribute('title');
979
- const path = typeof chipPath === 'string' ? chipPath.trim() : '';
980
- return path === '' ? null : { path: path, button: chip, produced: true };
981
- }
982
- const card = target.closest(CARD);
983
- if (card === null) return null;
984
- const overlay = card.querySelector('button[title]');
985
- if (overlay === null) return null;
986
- const overlayPath = overlay.getAttribute('title');
987
- const path = typeof overlayPath === 'string' ? overlayPath.trim() : '';
988
- return path === '' ? null : { path: path, button: overlay, produced: false };
989
- };
990
-
991
- /**
992
- * Run the product's own action for a surface by re-dispatching its click.
993
- * The flag lets that synthetic click past our capture handler, which
994
- * would otherwise open this menu again instead of the product's preview.
995
- */
996
- let replaying = false;
997
- const preview = (surface) => {
998
- replaying = true;
999
- try {
1000
- surface.button.click();
1001
- } finally {
1002
- replaying = false;
1003
- }
1004
- };
1005
-
1006
- let menu = null;
1007
- let toastNode = null;
1008
- const closeMenu = () => {
1009
- if (menu === null) return;
1010
- menu.remove();
1011
- menu = null;
1012
- };
1013
- const toast = (text) => {
1014
- if (toastNode !== null) toastNode.remove();
1015
- const node = document.createElement('div');
1016
- node.className = TOAST_CLS;
1017
- node.setAttribute('role', 'status');
1018
- node.textContent = text;
1019
- document.body.appendChild(node);
1020
- toastNode = node;
1021
- setTimeout(() => {
1022
- node.remove();
1023
- if (toastNode === node) toastNode = null;
1024
- }, 1800);
1025
- };
1026
- // localhost is a secure context, so the async clipboard exists in the
1027
- // real GUI; a rejection is still reported rather than swallowed.
1028
- const copyPath = (path) => {
1029
- const clipboard = navigator.clipboard;
1030
- if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') {
1031
- return Promise.resolve(false);
1032
- }
1033
- return Promise.resolve(clipboard.writeText(path)).then(() => true).catch(() => false);
1034
- };
1035
- const invoke = (endpoint, path) => Promise.resolve(bridgeCall(endpoint, { path: path }))
1036
- .then((result) => result !== undefined && result !== null && result.ok === true)
1037
- .catch(() => false);
1038
-
1039
- const buildItem = (icon, label, onPick) => {
1040
- const item = document.createElement('button');
1041
- item.type = 'button';
1042
- item.className = ITEM_CLS;
1043
- item.setAttribute('role', 'menuitem');
1044
- item.title = label;
1045
- item.setAttribute('aria-label', label);
1046
- const glyph = document.createElement('span');
1047
- glyph.innerHTML = icon;
1048
- const caption = document.createElement('span');
1049
- caption.textContent = label;
1050
- item.appendChild(glyph);
1051
- item.appendChild(caption);
1052
- item.addEventListener('click', (event) => {
1053
- event.preventDefault();
1054
- event.stopPropagation();
1055
- closeMenu();
1056
- onPick();
1057
- });
1058
- return item;
1059
- };
1060
-
1061
- const openMenu = (surface, clientX, clientY) => {
1062
- closeMenu();
1063
- const path = surface.path;
1064
- const text = labels();
1065
- const node = document.createElement('div');
1066
- node.className = CLS;
1067
- node.setAttribute('role', 'menu');
1068
- node.appendChild(buildItem(ICON_OPEN, text.open, () => {
1069
- invoke('dshome/file.open', path).then((ok) => { if (!ok) toast(text.failed); });
1070
- }));
1071
- node.appendChild(buildItem(ICON_REVEAL, text.reveal, () => {
1072
- invoke('dshome/file.reveal', path).then((ok) => { if (!ok) toast(text.failed); });
1073
- }));
1074
- node.appendChild(buildItem(ICON_COPY, text.copy, () => {
1075
- copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
1076
- }));
1077
- const separator = document.createElement('div');
1078
- separator.className = CLS + '-sep';
1079
- node.appendChild(separator);
1080
- node.appendChild(buildItem(ICON_PREVIEW, surface.produced ? text.previewProduced : text.preview, () => {
1081
- preview(surface);
1082
- }));
1083
- document.body.appendChild(node);
1084
- // Clamp to the viewport so a card near an edge never opens a menu
1085
- // the user cannot reach.
1086
- const box = node.getBoundingClientRect();
1087
- node.style.left = Math.max(8, Math.min(clientX, window.innerWidth - box.width - 8)) + 'px';
1088
- node.style.top = Math.max(8, Math.min(clientY, window.innerHeight - box.height - 8)) + 'px';
1089
- menu = node;
1090
- const first = node.querySelector('.' + ITEM_CLS);
1091
- if (first !== null) first.focus();
1092
- };
1093
-
1094
- const onContextMenu = (event) => {
1095
- const surface = surfaceOf(event.target);
1096
- if (surface === null) return;
1097
- event.preventDefault();
1098
- event.stopPropagation();
1099
- openMenu(surface, event.clientX, event.clientY);
1100
- };
1101
- // A changed-file chip's only own action is the product's preview, and
1102
- // in practice clicking one looks like nothing happens. Those chips
1103
- // therefore open this menu on a plain left click, with the product's
1104
- // own action kept as a menu item. Presented cards keep their left
1105
- // click — their overlay visibly previews — and use the right click.
1106
- const onClickCapture = (event) => {
1107
- if (replaying || event.defaultPrevented) return;
1108
- if (event.button !== undefined && event.button !== 0) return;
1109
- const surface = surfaceOf(event.target);
1110
- if (surface === null || !surface.produced) return;
1111
- event.preventDefault();
1112
- event.stopPropagation();
1113
- openMenu(surface, event.clientX, event.clientY);
1114
- };
1115
- const onPointerDown = (event) => {
1116
- if (menu === null) return;
1117
- if (event.target instanceof Node && menu.contains(event.target)) return;
1118
- closeMenu();
1119
- };
1120
- const onKeyDown = (event) => {
1121
- if (event.key === 'Escape') closeMenu();
1122
- };
1123
- document.addEventListener('contextmenu', onContextMenu, true);
1124
- document.addEventListener('click', onClickCapture, true);
1125
- document.addEventListener('pointerdown', onPointerDown, true);
1126
- document.addEventListener('keydown', onKeyDown, true);
1127
- window.addEventListener('resize', closeMenu, true);
1128
- window.addEventListener('scroll', closeMenu, true);
1129
-
1130
- // "Copy path" is the one gesture the product lacks, so it is also added
1131
- // to the product's own chevron menu, where file actions are looked for.
1132
- let anchorCard = null;
1133
- const track = (target) => {
1134
- anchorCard = null;
1135
- if (target === null || typeof target.closest !== 'function') return;
1136
- const button = target.closest('button');
1137
- if (button === null || button.getAttribute('aria-haspopup') !== 'menu') return;
1138
- const card = button.closest(CARD);
1139
- if (card === null) return;
1140
- anchorCard = card;
1141
- };
1142
- const onTrackCapture = (event) => track(event.target);
1143
- document.addEventListener('click', onTrackCapture, true);
1144
- document.addEventListener('focusin', onTrackCapture, true);
1145
-
1146
- const injectCopyItem = (node) => {
1147
- if (anchorCard === null || !document.contains(anchorCard)) return;
1148
- if (node.querySelector('.' + COPY_CLS) !== null) return;
1149
- const card = anchorCard;
1150
- const path = pathOf(card);
1151
- if (path === '') return;
1152
- const viewport = node.querySelector('[role="presentation"]');
1153
- if (viewport === null) return;
1154
- const text = labels();
1155
- const item = document.createElement('button');
1156
- item.type = 'button';
1157
- item.className = COPY_CLS;
1158
- item.setAttribute('role', 'menuitem');
1159
- item.title = text.copy;
1160
- item.setAttribute('aria-label', text.copy);
1161
- const glyph = document.createElement('span');
1162
- glyph.innerHTML = ICON_COPY;
1163
- const caption = document.createElement('span');
1164
- caption.textContent = text.copy;
1165
- item.appendChild(glyph);
1166
- item.appendChild(caption);
1167
- item.addEventListener('click', (event) => {
1168
- event.preventDefault();
1169
- event.stopPropagation();
1170
- copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
1171
- try {
1172
- document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
1173
- } catch (error) {}
1174
- });
1175
- viewport.appendChild(item);
1176
- };
1177
-
1178
- const observer = new MutationObserver((records) => {
1179
- for (const record of records) {
1180
- for (const added of record.addedNodes) {
1181
- if (!(added instanceof Element)) continue;
1182
- if (added.matches(MENU)) {
1183
- injectCopyItem(added);
1184
- continue;
1185
- }
1186
- added.querySelectorAll(MENU).forEach(injectCopyItem);
1187
- }
1188
- }
1189
- });
1190
- observer.observe(document.body, { childList: true, subtree: true });
1191
-
1192
- const disposeStyle = injectStyle(CSS, 'file-card-menu.css');
1193
- return () => {
1194
- disposeStyle();
1195
- observer.disconnect();
1196
- closeMenu();
1197
- if (toastNode !== null) toastNode.remove();
1198
- document.removeEventListener('contextmenu', onContextMenu, true);
1199
- document.removeEventListener('click', onClickCapture, true);
1200
- document.removeEventListener('pointerdown', onPointerDown, true);
1201
- document.removeEventListener('keydown', onKeyDown, true);
1202
- document.removeEventListener('click', onTrackCapture, true);
1203
- document.removeEventListener('focusin', onTrackCapture, true);
1204
- window.removeEventListener('resize', closeMenu, true);
1205
- window.removeEventListener('scroll', closeMenu, true);
1206
- };
1207
- }
1208
-
1209
- /**
1210
- * "Running subagents" floating panel. While any subagent-origin session in
1211
- * the sessions list mirror reports `running`, the panel shows every such
1212
- * subagent with live progress: elapsed active time folded from the
1213
- * `subagentTiming` projection (settled + open-turn span), token usage
1214
- * folded from the `tokenUsage` projection, and an indeterminate shimmer
1215
- * bar. Clicking a row opens that subagent session (catalog address when
1216
- * retained, plain open otherwise). Reads only leaf fields of the live
1217
- * list snapshot; every side effect (DOM, subscription, timers) is owned
1218
- * by the returned disposer.
1219
- */
1220
- function watchRunningSubagents(ctx) {
1221
- let sessions;
1222
- try {
1223
- sessions = ctx.get('sessions');
1224
- } catch (error) {
1225
- sessions = undefined;
1226
- }
1227
- if (
1228
- sessions === undefined || sessions === null ||
1229
- sessions.list === undefined || sessions.list === null ||
1230
- typeof sessions.list.getSnapshot !== 'function' ||
1231
- typeof sessions.list.subscribe !== 'function'
1232
- ) {
1233
- return () => {};
1234
- }
1235
-
1236
- let locale;
1237
- try {
1238
- locale = ctx.get('locale');
1239
- } catch (error) {
1240
- locale = undefined;
1241
- }
1242
- let connection;
1243
- try {
1244
- connection = ctx.get('connection');
1245
- } catch (error) {
1246
- connection = undefined;
1247
- }
1248
- const isZh = () => {
1249
- try {
1250
- if (locale === undefined || locale === null || locale.getLocale === undefined) return true;
1251
- const active = locale.getLocale().active;
1252
- return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
1253
- } catch (error) {
1254
- return true;
1255
- }
1256
- };
1257
-
1258
- const tokenTotal = (usage) => {
1259
- if (usage === undefined || usage === null) return undefined;
1260
- return (
1261
- (Number(usage.uncachedInputTokens) || 0) +
1262
- (Number(usage.outputTokens) || 0) +
1263
- (Number(usage.cacheReadTokens) || 0) +
1264
- (Number(usage.cacheWriteTokens) || 0)
1265
- );
1266
- };
1267
- const formatTokens = (value) => {
1268
- const scaled = (next) => (next >= 100 ? String(Math.round(next)) : String(Math.round(next * 10) / 10));
1269
- if (value < 1000) return String(value);
1270
- if (value < 1e6) return scaled(value / 1e3) + 'K';
1271
- return scaled(value / 1e6) + 'M';
1272
- };
1273
- const formatDuration = (ms) => {
1274
- const total = Math.max(0, Math.floor(ms / 1000));
1275
- const seconds = total % 60;
1276
- const minutes = Math.floor(total / 60) % 60;
1277
- const hours = Math.floor(total / 3600);
1278
- if (isZh()) {
1279
- if (hours > 0) return hours + '小时' + String(minutes).padStart(2, '0') + '分';
1280
- if (minutes > 0) return minutes + '分' + String(seconds).padStart(2, '0') + '秒';
1281
- return seconds + '秒';
1282
- }
1283
- if (hours > 0) return hours + 'h ' + String(minutes).padStart(2, '0') + 'm';
1284
- if (minutes > 0) return minutes + 'm ' + String(seconds).padStart(2, '0') + 's';
1285
- return seconds + 's';
1286
- };
1287
- const runningTitle = (count) => (
1288
- isZh()
1289
- ? count + ' 个子代理正在运行'
1290
- : (count === 1 ? '1 subagent running' : count + ' subagents running')
1291
- );
1292
-
1293
- const styleTag = document.createElement('style');
1294
- styleTag.dataset.plugin = PLUGIN_ID;
1295
- styleTag.dataset.pluginCss = PLUGIN_ID + '/subagent-panel.css';
1296
- styleTag.textContent = SUBAGENT_PANEL_CSS;
1297
- document.head.appendChild(styleTag);
1298
-
1299
- const CHEVRON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3.5 5 6.5 8 3.5"/></svg>';
1300
-
1301
- const panel = document.createElement('div');
1302
- panel.className = 'dshome-sa';
1303
- panel.hidden = true;
1304
- panel.setAttribute('role', 'status');
1305
- panel.setAttribute('aria-live', 'polite');
1306
-
1307
- const head = document.createElement('div');
1308
- head.className = 'dshome-sa-head';
1309
- const pulse = document.createElement('span');
1310
- pulse.className = 'dshome-sa-pulse';
1311
- const titleEl = document.createElement('span');
1312
- titleEl.className = 'dshome-sa-title';
1313
- const collapseBtn = document.createElement('button');
1314
- collapseBtn.type = 'button';
1315
- collapseBtn.className = 'dshome-sa-collapse';
1316
- collapseBtn.innerHTML = CHEVRON_SVG;
1317
- head.appendChild(pulse);
1318
- head.appendChild(titleEl);
1319
- head.appendChild(collapseBtn);
1320
-
1321
- const listEl = document.createElement('div');
1322
- listEl.className = 'dshome-sa-list';
1323
- panel.appendChild(head);
1324
- panel.appendChild(listEl);
1325
- document.body.appendChild(panel);
1326
-
1327
- const timingOf = (s) => (
1328
- s.projectionValues === undefined || s.projectionValues === null
1329
- ? undefined
1330
- : s.projectionValues.subagentTiming
1331
- );
1332
- const startOf = (s) => {
1333
- const t = timingOf(s);
1334
- const since = t === undefined || t === null || t.active === undefined || t.active === null ? undefined : t.active.since;
1335
- if (typeof since === 'number') return since;
1336
- return typeof s.updatedAt === 'number' ? s.updatedAt : 0;
1337
- };
1338
- const elapsedMs = (s) => {
1339
- const t = timingOf(s);
1340
- if (t === undefined || t === null) return undefined;
1341
- const settled = typeof t.settledMs === 'number' ? t.settledMs : 0;
1342
- const since = t.active === undefined || t.active === null ? undefined : t.active.since;
1343
- if (typeof since !== 'number') return settled;
1344
- return settled + Math.max(0, Date.now() - since);
1345
- };
1346
- const metaText = (s) => {
1347
- const parts = [];
1348
- const elapsed = elapsedMs(s);
1349
- if (elapsed !== undefined) parts.push((isZh() ? '已运行 ' : '') + formatDuration(elapsed));
1350
- const tokens = tokenTotal(
1351
- s.projectionValues === undefined || s.projectionValues === null
1352
- ? undefined
1353
- : s.projectionValues.tokenUsage
1354
- );
1355
- if (tokens !== undefined) parts.push(formatTokens(tokens) + ' tok');
1356
- if (parts.length === 0) parts.push(isZh() ? '正在运行…' : 'running…');
1357
- return parts.join(' · ');
1358
- };
1359
-
1360
- const runningSubagents = () => {
1361
- try {
1362
- const snapshot = sessions.list.getSnapshot();
1363
- if (snapshot === undefined || snapshot === null || snapshot.byId === undefined || snapshot.byId === null) return [];
1364
- const items = [];
1365
- for (const key of Object.keys(snapshot.byId)) {
1366
- const s = snapshot.byId[key];
1367
- if (s === null || s === undefined) continue;
1368
- if (s.running !== true || s.origin !== 'subagent') continue;
1369
- items.push(s);
1370
- }
1371
- items.sort((a, b) => startOf(a) - startOf(b));
1372
- return items;
1373
- } catch (error) {
1374
- return [];
1375
- }
1376
- };
1377
-
1378
- const openSubagent = (s) => {
1379
- try {
1380
- let address;
1381
- if (typeof sessions.subagentAddress === 'function') address = sessions.subagentAddress(s.id);
1382
- if (address !== undefined && address !== null && typeof sessions.openSubagent === 'function') sessions.openSubagent(address);
1383
- else sessions.open(s.id);
1384
- } catch (error) {
1385
- try { sessions.open(s.id); } catch (inner) {}
1386
- }
1387
- };
1388
-
1389
- const STOP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><rect x="1" y="1" width="8" height="8" rx="2"/></svg>';
1390
-
1391
- const stopTitle = () => (isZh() ? '停止此子代理' : 'Stop this subagent');
1392
-
1393
- const rows = new Map();
1394
- const buildRow = (s) => {
1395
- const item = document.createElement('div');
1396
- item.className = 'dshome-sa-item';
1397
- item.setAttribute('role', 'button');
1398
- item.tabIndex = 0;
1399
- const sweep = document.createElement('span');
1400
- sweep.className = 'dshome-sa-sweep';
1401
- item.appendChild(sweep);
1402
- const dot = document.createElement('span');
1403
- dot.className = 'dshome-sa-dot';
1404
- const body = document.createElement('span');
1405
- body.className = 'dshome-sa-body';
1406
- const name = document.createElement('span');
1407
- name.className = 'dshome-sa-name';
1408
- const meta = document.createElement('span');
1409
- meta.className = 'dshome-sa-meta';
1410
- const bar = document.createElement('span');
1411
- bar.className = 'dshome-sa-bar';
1412
- const fill = document.createElement('span');
1413
- fill.className = 'dshome-sa-bar-fill';
1414
- bar.appendChild(fill);
1415
- body.appendChild(name);
1416
- body.appendChild(meta);
1417
- body.appendChild(bar);
1418
- item.appendChild(dot);
1419
- item.appendChild(body);
1420
-
1421
- const stopBtn = document.createElement('button');
1422
- stopBtn.type = 'button';
1423
- stopBtn.className = 'dshome-sa-stop';
1424
- stopBtn.innerHTML = STOP_SVG;
1425
- stopBtn.title = stopTitle();
1426
- stopBtn.setAttribute('aria-label', stopTitle());
1427
- stopBtn.addEventListener('click', (event) => stopSubagent(s, stopBtn, event));
1428
- item.appendChild(stopBtn);
1429
-
1430
- item.addEventListener('click', () => openSubagent(s));
1431
- item.addEventListener('keydown', (event) => {
1432
- if (event.key === 'Enter' || event.key === ' ') {
1433
- event.preventDefault();
1434
- openSubagent(s);
1435
- }
1436
- });
1437
- return {
1438
- root: item,
1439
- nameEl: name,
1440
- metaEl: meta,
1441
- stopEl: stopBtn,
1442
- sweepEl: sweep,
1443
- summary: s,
1444
- phase: 'running',
1445
- progress: 0,
1446
- stopStart: 0,
1447
- completeStart: 0,
1448
- fillFrom: 0,
1449
- exitTimer: 0
1450
- };
1451
- };
1452
-
1453
- const deniedTimers = new Set();
1454
- const flashDenied = (btn) => {
1455
- btn.classList.add('dshome-sa-stop-denied');
1456
- const timer = setTimeout(() => {
1457
- deniedTimers.delete(timer);
1458
- btn.classList.remove('dshome-sa-stop-denied');
1459
- }, 1200);
1460
- deniedTimers.add(timer);
1461
- };
1462
-
1463
- const easeOutCubic = (k) => 1 - Math.pow(1 - k, 3);
1464
- /**
1465
- * Fake stopping progress: sprint to 80% within the first second,
1466
- * then crawl asymptotically toward 95% — never 100% on its own;
1467
- * termination is what completes the bar.
1468
- */
1469
- const stoppingProgress = (elapsedSec) => {
1470
- if (elapsedSec <= 1) return 0.8 * easeOutCubic(elapsedSec);
1471
- return Math.min(0.95, 0.8 + 0.15 * (1 - Math.exp(-(elapsedSec - 1) / 3)));
1472
- };
1473
-
1474
- let sweepRaf = 0;
1475
- const sweepTick = () => {
1476
- const now = performance.now();
1477
- let any = false;
1478
- for (const row of rows.values()) {
1479
- if (row.phase === 'stopping') {
1480
- any = true;
1481
- row.progress = stoppingProgress((now - row.stopStart) / 1000);
1482
- row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
1483
- } else if (row.phase === 'completing') {
1484
- any = true;
1485
- const t = (now - row.completeStart) / 1000;
1486
- if (t < 0.3) {
1487
- const k = easeOutCubic(Math.min(1, t / 0.3));
1488
- row.progress = row.fillFrom + (1 - row.fillFrom) * k;
1489
- row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
1490
- } else if (t < 0.6) {
1491
- row.sweepEl.style.width = '100%';
1492
- } else {
1493
- row.phase = 'exiting';
1494
- row.root.classList.add('dshome-sa-item-exit');
1495
- row.exitTimer = setTimeout(() => {
1496
- row.root.remove();
1497
- rows.delete(row.summary.id);
1498
- syncTicker();
1499
- }, 240);
1500
- }
1501
- }
1502
- }
1503
- if (any) sweepRaf = requestAnimationFrame(sweepTick);
1504
- else sweepRaf = 0;
1505
- };
1506
-
1507
- const hasStopping = () => {
1508
- for (const row of rows.values()) {
1509
- if (row.phase === 'stopping' || row.phase === 'completing' || row.phase === 'exiting') return true;
1510
- }
1511
- return false;
1512
- };
1513
-
1514
- const startStopProgress = (row) => {
1515
- row.phase = 'stopping';
1516
- row.progress = 0;
1517
- row.stopStart = performance.now();
1518
- row.root.classList.add('dshome-sa-stopping');
1519
- row.stopEl.classList.remove('dshome-sa-stop-busy');
1520
- row.sweepEl.style.width = '0%';
1521
- if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
1522
- };
1523
-
1524
- /** Undo the optimistic stopping state when the cancel call is rejected. */
1525
- const rollbackStop = (row, btn) => {
1526
- if (btn !== undefined && btn !== null) {
1527
- btn.disabled = false;
1528
- btn.classList.remove('dshome-sa-stop-busy');
1529
- flashDenied(btn);
1530
- }
1531
- if (row === undefined) return;
1532
- if (rows.get(row.summary.id) !== row) return;
1533
- if (row.phase !== 'stopping') return;
1534
- row.phase = 'running';
1535
- row.progress = 0;
1536
- row.root.classList.remove('dshome-sa-stopping');
1537
- row.sweepEl.style.width = '0%';
1538
- };
1539
-
1540
- const beginCompleting = (row) => {
1541
- row.phase = 'completing';
1542
- row.completeStart = performance.now();
1543
- row.fillFrom = row.progress;
1544
- if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
1545
- };
1546
-
1547
- /**
1548
- * Durable parent/child address for the subagents.interrupt RPC.
1549
- * The mode comes from the child's own identity projection; a parent id
1550
- * is required — generic host routing rejects subagent-owned sessions,
1551
- * so the interrupt must ride the address, not sessions.cancel.
1552
- */
1553
- const addressOf = (s) => {
1554
- const identity = s.projectionValues === undefined || s.projectionValues === null ? undefined : s.projectionValues.subagent;
1555
- let mode = 'continuable';
1556
- if (identity !== undefined && identity !== null && (identity.mode === 'one-shot' || identity.mode === 'continuable')) mode = identity.mode;
1557
- if (typeof s.parentId !== 'string' || s.parentId === '') return undefined;
1558
- return { parentSessionId: s.parentId, childSessionId: s.id, mode };
1559
- };
1560
-
1561
- const stopSubagent = (s, btn, event) => {
1562
- if (event !== undefined && event !== null) {
1563
- event.preventDefault();
1564
- event.stopPropagation();
1565
- }
1566
- const row = rows.get(s.id);
1567
- if (row !== undefined && row.phase !== 'running') return;
1568
- let face;
1569
- try {
1570
- const binding = sessions.binding(s.id);
1571
- face = binding === undefined || binding === null ? undefined : binding.session;
1572
- } catch (error) {
1573
- face = undefined;
1574
- }
1575
- const address = addressOf(s);
1576
- const api = connection === undefined || connection === null ? undefined : connection.api;
1577
- const canInterrupt = address !== undefined && address.mode === 'continuable' && api !== undefined && api !== null && api.subagents !== undefined && api.subagents !== null && typeof api.subagents.interrupt === 'function';
1578
- const canFaceCancel = face !== undefined && face !== null && typeof face.cancel === 'function';
1579
- if (!canInterrupt && !canFaceCancel) {
1580
- flashDenied(btn);
1581
- return;
1582
- }
1583
- btn.disabled = true;
1584
- // Optimistic feedback: the red line + sweep start the moment the
1585
- // button is pressed; a rejected cancel rolls them back.
1586
- if (row !== undefined) startStopProgress(row);
1587
- let cancelPromise;
1588
- try {
1589
- cancelPromise = canInterrupt
1590
- ? Promise.resolve(api.subagents.interrupt(address))
1591
- : Promise.resolve(face.cancel());
1592
- } catch (error) {
1593
- rollbackStop(row, btn);
1594
- return;
1595
- }
1596
- cancelPromise.then((result) => {
1597
- const ok = result !== undefined && result !== null && result.ok === true;
1598
- if (!ok) rollbackStop(row, btn);
1599
- }).catch(() => {
1600
- rollbackStop(row, btn);
1601
- });
1602
- };
1603
-
1604
- let hideTimer = 0;
1605
- const hide = () => {
1606
- if (panel.hidden || hideTimer !== 0) return;
1607
- panel.classList.add('dshome-sa-leave');
1608
- hideTimer = setTimeout(() => {
1609
- hideTimer = 0;
1610
- panel.hidden = true;
1611
- panel.classList.remove('dshome-sa-leave');
1612
- }, 240);
1613
- };
1614
-
1615
- let tickTimer = 0;
1616
- const tick = () => {
1617
- if (panel.hidden) return;
1618
- for (const row of rows.values()) {
1619
- if (row.summary.running !== true) continue;
1620
- row.metaEl.textContent = metaText(row.summary);
1621
- }
1622
- };
1623
- const syncTicker = () => {
1624
- if (rows.size > 0 && tickTimer === 0) {
1625
- tickTimer = setInterval(tick, 1000);
1626
- } else if (rows.size === 0 && tickTimer !== 0) {
1627
- clearInterval(tickTimer);
1628
- tickTimer = 0;
1629
- }
1630
- };
1631
-
1632
- let collapsed = false;
1633
- const updateCollapsed = () => {
1634
- panel.classList.toggle('dshome-sa-collapsed', collapsed);
1635
- const label = collapsed ? (isZh() ? '展开' : 'Expand') : (isZh() ? '收起' : 'Collapse');
1636
- collapseBtn.setAttribute('aria-label', label);
1637
- collapseBtn.title = label;
1638
- };
1639
- collapseBtn.addEventListener('click', () => {
1640
- collapsed = !collapsed;
1641
- updateCollapsed();
1642
- });
1643
-
1644
- const render = () => {
1645
- const items = runningSubagents();
1646
- const stopping = hasStopping();
1647
- if (items.length === 0 && !stopping) {
1648
- titleEl.textContent = runningTitle(0);
1649
- hide();
1650
- return;
1651
- }
1652
- titleEl.textContent = items.length > 0 ? runningTitle(items.length) : (isZh() ? '正在停止子代理…' : 'Stopping subagent…');
1653
- if (hideTimer !== 0) {
1654
- clearTimeout(hideTimer);
1655
- hideTimer = 0;
1656
- }
1657
- panel.classList.remove('dshome-sa-leave');
1658
- panel.hidden = false;
1659
- const seen = new Set();
1660
- for (const s of items) {
1661
- seen.add(s.id);
1662
- let row = rows.get(s.id);
1663
- if (row === undefined) {
1664
- row = buildRow(s);
1665
- rows.set(s.id, row);
1666
- listEl.appendChild(row.root);
1667
- }
1668
- row.summary = s;
1669
- const name = s.displayTitle || s.id;
1670
- row.nameEl.textContent = name;
1671
- row.root.title = name;
1672
- row.metaEl.textContent = metaText(s);
1673
- }
1674
- for (const entry of Array.from(rows.entries())) {
1675
- const id = entry[0];
1676
- const row = entry[1];
1677
- if (seen.has(id)) continue;
1678
- if (row.phase === 'stopping') {
1679
- beginCompleting(row);
1680
- continue;
1681
- }
1682
- if (row.phase === 'completing' || row.phase === 'exiting') continue;
1683
- row.root.remove();
1684
- rows.delete(id);
1685
- }
1686
- syncTicker();
1687
- };
1688
-
1689
- const unsubscribe = sessions.list.subscribe(render);
1690
- render();
1691
- updateCollapsed();
1692
-
1693
- return () => {
1694
- unsubscribe();
1695
- if (sweepRaf !== 0) cancelAnimationFrame(sweepRaf);
1696
- if (tickTimer !== 0) clearInterval(tickTimer);
1697
- if (hideTimer !== 0) clearTimeout(hideTimer);
1698
- for (const row of rows.values()) {
1699
- if (row.exitTimer !== 0) clearTimeout(row.exitTimer);
1700
- }
1701
- for (const timer of deniedTimers) clearTimeout(timer);
1702
- deniedTimers.clear();
1703
- panel.remove();
1704
- styleTag.remove();
1705
- };
1706
- }
1707
-
1708
- function LocaleToggle(props) {
1709
- const locale = props.locale;
1710
- const [active, setActive] = React.useState('zh');
1711
-
1712
- React.useEffect(() => {
1713
- if (locale === undefined) return;
1714
- const sync = () => {
1715
- try { setActive(locale.getLocale().active); } catch (e) {}
1716
- };
1717
- sync();
1718
- return locale.subscribe(sync);
1719
- }, [locale]);
1720
-
1721
- const pick = (id) => () => {
1722
- if (locale === undefined) return;
1723
- try { locale.setLocale(id); } catch (e) {}
1724
- };
1725
-
1726
- const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
1727
-
1728
- return React.createElement('div', { className: 'dshome-locale', role: 'group', 'aria-label': 'Language' },
1729
- React.createElement('button', { type: 'button', className: isZh ? 'dshome-on' : '', onClick: pick('zh') },
1730
- React.createElement('span', { className: 'dshome-full' }, '中文'),
1731
- React.createElement('span', { className: 'dshome-short' }, '中')),
1732
- React.createElement('button', { type: 'button', className: !isZh ? 'dshome-on' : '', onClick: pick('en') },
1733
- React.createElement('span', { className: 'dshome-full' }, 'EN'),
1734
- React.createElement('span', { className: 'dshome-short' }, 'E'))
1735
- );
1736
- }
1737
-
1738
- // ── DSTT (DeepSeekStyleTheme) settings + peak-hour color driver ──────
1739
- // The host half owns the durable `deepseek-style-theme` settings
1740
- // namespace; the browser half reads/writes one `mode` enum over this
1741
- // plugin's private loopback channel — the settings RPC surface only
1742
- // serves the core's allowlisted namespaces. A shared in-memory mirror
1743
- // feeds the event-chain clock, which checks the Beijing time once, then
1744
- // schedules exactly the next boundary switch — vivid red (鲜红) inside a
1745
- // peak window (Beijing 9:00–12:00, 14:00–18:00), blue or green outside
1746
- // per mode. One-minute granularity (ceiling) per the user's rule.
1747
- const DSTT_NS = "deepseek-style-theme";
1748
- const DSTT_FIELD = "mode";
1749
- const DSTT_MODES = ["peakvalley-redblue", "peakvalley-redgreen", "always-green", "always-blue"];
1750
- const DSTT_LABELS = {
1751
- "peakvalley-redblue": { zh: "峰谷红蓝", en: "Peak R·B" },
1752
- "peakvalley-redgreen": { zh: "峰谷红绿", en: "Peak R·G" },
1753
- "always-green": { zh: "常态绿", en: "Always G" },
1754
- "always-blue": { zh: "常态蓝", en: "Always B" }
1755
- };
1756
- /** DSTT private-channel endpoints (host half in lib/index.js). */
1757
- const DSTT_GET = "dstt.mode.get";
1758
- const DSTT_SET = "dstt.mode.set";
1759
- /** Peak windows, [startHour, endHour), judged in Beijing time (UTC+8). */
1760
- const PEAK_RANGES = [[9, 12], [14, 18]];
1761
-
1762
- /**
1763
- * Shared in-memory mirror of the durable DSTT mode. The settings domain's
1764
- * RPC only serves allowlisted namespaces, so the mode travels over this
1765
- * plugin's own loopback channel instead; every reader (the color driver,
1766
- * the settings tab) subscribes to this mirror.
1767
- */
1768
- const dsttState = { mode: "peakvalley-redblue", listeners: new Set() };
1769
- const dsttGetMode = () => dsttState.mode;
1770
- const dsttSubscribe = (listener) => {
1771
- dsttState.listeners.add(listener);
1772
- return () => dsttState.listeners.delete(listener);
1773
- };
1774
- const dsttNotify = () => {
1775
- for (const listener of dsttState.listeners) {
1776
- try { listener(); } catch (e) {}
1777
- }
1778
- };
1779
- /** Path of this plugin's private host bridge (a webServer prefix route). */
1780
- const BRIDGE_PATH = '/dshome-open-workspace';
1781
-
1782
- /**
1783
- * Call the host bridge. The host half owns a plain `webServer` prefix route
1784
- * (dsh 0.1.5's connection carrier cannot host this plugin's channel without
1785
- * tripping cordis' webServer guard), reached with a same-origin fetch so the
1786
- * browser session authenticates it like any other web-app request. Resolves
1787
- * to the RpcResult-shaped JSON, throws on a transport failure.
1788
- */
1789
- async function bridgeCall(endpoint, payload) {
1790
- const response = await fetch(BRIDGE_PATH, {
1791
- method: 'POST',
1792
- headers: { 'content-type': 'application/json' },
1793
- body: JSON.stringify({ endpoint: endpoint, payload: payload })
1794
- });
1795
- if (!response.ok) throw new Error('bridge http ' + response.status);
1796
- return await response.json();
1797
- }
1798
-
1799
- /** Pull the durable mode from the host half once. */
1800
- async function dsttSync(ctx) {
1801
- try {
1802
- const result = await bridgeCall(DSTT_GET, {});
1803
- if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
1804
- dsttState.mode = result.value.mode;
1805
- dsttNotify();
1806
- }
1807
- } catch (error) {}
1808
- }
1809
- /** Persist a mode choice through the host half; resolves with success. */
1810
- async function dsttSetMode(ctx, mode) {
1811
- if (!DSTT_MODES.includes(mode)) return false;
1812
- try {
1813
- const result = await bridgeCall(DSTT_SET, { mode });
1814
- if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
1815
- dsttState.mode = result.value.mode;
1816
- dsttNotify();
1817
- return true;
1818
- }
1819
- } catch (error) {}
1820
- return false;
1821
- }
1822
-
1823
- /**
1824
- * Current time shifted onto the Beijing clock. Date#getTime() is already a
1825
- * UTC instant, so Beijing time is exactly `now + 8h` regardless of the
1826
- * machine's own timezone — `getTimezoneOffset()` must NOT be involved
1827
- * (adding it double-counts on UTC+8 machines and reads the wrong hour).
1828
- * Beijing has no DST, so the +8h shift is exact.
1829
- */
1830
- function beijingNow() {
1831
- return new Date(Date.now() + 8 * 3600000);
1832
- }
1833
-
1834
- /** Beijing wall-clock HH:MM:SS for logs. */
1835
- function beijingClock(bj) {
1836
- const pad = (n) => String(n).padStart(2, '0');
1837
- return pad(bj.getUTCHours()) + ':' + pad(bj.getUTCMinutes()) + ':' + pad(bj.getUTCSeconds()) + ' (UTC+8)';
1838
- }
1839
-
1840
- /**
1841
- * Round a Beijing time UP to the next whole minute (ceiling on the second
1842
- * field). One-minute granularity: 12:59:17 reads as 13:00 → valley, and
1843
- * the moment the wall clock hits 14:00 the red takes over; 13:59:30
1844
- * already counts as 14:00 (inside the window) because it rounds up.
1845
- */
1846
- function ceilToMinute(beijingDate) {
1847
- const rounded = new Date(beijingDate.getTime());
1848
- if (rounded.getUTCSeconds() > 0 || rounded.getUTCMilliseconds() > 0) {
1849
- rounded.setUTCSeconds(0, 0);
1850
- rounded.setUTCMinutes(rounded.getUTCMinutes() + 1);
1851
- }
1852
- return rounded;
1853
- }
1854
-
1855
- /** Whether a rounded Beijing minute falls inside a peak window. Weekends
1856
- * (Beijing Saturday/Sunday) are entirely off-peak. */
1857
- function isPeakMinute(beijingDate) {
1858
- const day = beijingDate.getUTCDay();
1859
- if (day === 0 || day === 6) return false;
1860
- const hour = beijingDate.getUTCHours();
1861
- return PEAK_RANGES.some(([from, to]) => hour >= from && hour < to);
1862
- }
1863
-
1864
- /** Next Beijing wall-clock occurrence of `hour` (today if still ahead, else tomorrow). */
1865
- function nextBoundary(beijingDate, hour) {
1866
- const next = new Date(beijingDate.getTime());
1867
- next.setUTCHours(hour, 0, 0, 0);
1868
- if (next.getTime() <= beijingDate.getTime()) next.setUTCDate(next.getUTCDate() + 1);
1869
- return next;
1870
- }
1871
-
1872
- /** The boundary that ends the peak window `beijingDate` is inside, or null. */
1873
- function endOfWindow(beijingDate) {
1874
- const hour = beijingDate.getUTCHours();
1875
- for (const [from, to] of PEAK_RANGES) {
1876
- if (hour >= from && hour < to) return nextBoundary(beijingDate, to);
1877
- }
1878
- return null;
1879
- }
1880
-
1881
- /** The earliest future peak-window start, or null if none (defensive). */
1882
- function nextStart(beijingDate) {
1883
- const candidates = PEAK_RANGES
1884
- .map(([from]) => nextBoundary(beijingDate, from))
1885
- .filter((d) => d.getTime() > beijingDate.getTime())
1886
- .sort((a, b) => a - b);
1887
- return candidates.length === 0 ? null : candidates[0];
1888
- }
1889
-
1890
- /** Color-change listeners (particles, UI re-render on switch). */
1891
- const colorListeners = new Set();
1892
- const subscribeColor = (fn) => {
1893
- colorListeners.add(fn);
1894
- return () => colorListeners.delete(fn);
1895
- };
1896
- const notifyColor = (color) => {
1897
- for (const fn of colorListeners) {
1898
- try { fn(color); } catch (e) {}
1899
- }
1900
- };
1901
- const isTimeSliced = (mode) => mode === "peakvalley-redblue" || mode === "peakvalley-redgreen";
1902
- const colorFor = (mode, peak) => {
1903
- if (mode === "always-blue") return "blue";
1904
- if (mode === "always-green") return "green";
1905
- return peak ? "red" : (mode === "peakvalley-redgreen" ? "green" : "blue");
1906
- };
1907
-
1908
- /**
1909
- * Color driver — event chain, no polling. On start it checks the
1910
- * Beijing clock once, then schedules exactly the next boundary switch:
1911
- * inside a window → vivid red now, record the delta to the window end,
1912
- * flip back to the valley color when it elapses; outside → valley color,
1913
- * flip to red at the next window start. Toggling the DSTT mode
1914
- * re-anchors immediately; closing the feature tears every timer down. A
1915
- * `visibilitychange` re-anchors the chain from the live clock in case a
1916
- * throttled background tab drifted.
1917
- */
1918
- function watchPeakHour(ctx) {
1919
- let chainTimer = 0;
1920
- let stopped = false;
1921
- let unsubscribe = null;
1922
-
1923
- const applyColor = () => {
1924
- if (stopped) return;
1925
- const mode = dsttGetMode();
1926
- const peak = isTimeSliced(mode) && isPeakMinute(ceilToMinute(beijingNow()));
1927
- const color = colorFor(mode, peak);
1928
- if (document.body.getAttribute('data-dshome-color') !== color) {
1929
- document.body.setAttribute('data-dshome-color', color);
1930
- console.info('[' + PLUGIN_ID + '] color -> ' + color + (peak ? ' (peak)' : ' (valley)') + ' @ ' + beijingClock(beijingNow()));
1931
- notifyColor(color);
1932
- }
1933
- };
1934
-
1935
- const schedule = (ms) => {
1936
- if (chainTimer !== 0) {
1937
- clearTimeout(chainTimer);
1938
- chainTimer = 0;
1939
- }
1940
- // setTimeout caps near 24.8 days; the longest span here (to
1941
- // tomorrow's first window start) is far below that. The +500ms
1942
- // margin re-anchors cleanly across the minute boundary.
1943
- chainTimer = setTimeout(anchor, Math.max(0, ms) + 500);
1944
- };
1945
-
1946
- /** The single entry point: read the mode + Beijing clock, decide, schedule. */
1947
- const anchor = () => {
1948
- if (stopped) return;
1949
- applyColor();
1950
- const mode = dsttGetMode();
1951
- if (!isTimeSliced(mode)) return;
1952
- const now = ceilToMinute(beijingNow());
1953
- const end = endOfWindow(now);
1954
- if (end !== null) {
1955
- schedule(end.getTime() - now.getTime());
1956
- } else {
1957
- const start = nextStart(now);
1958
- if (start !== null) schedule(start.getTime() - now.getTime());
1959
- }
1960
- };
1961
-
1962
- const onVisibility = () => {
1963
- if (!document.hidden) anchor();
1964
- };
1965
- document.addEventListener('visibilitychange', onVisibility);
1966
-
1967
- anchor();
1968
-
1969
- try { unsubscribe = dsttSubscribe(anchor); } catch (error) { unsubscribe = null; }
1970
-
1971
- return () => {
1972
- stopped = true;
1973
- if (chainTimer !== 0) clearTimeout(chainTimer);
1974
- if (unsubscribe !== null) unsubscribe();
1975
- document.removeEventListener('visibilitychange', onVisibility);
1976
- };
1977
- }
1978
-
1979
- /**
1980
- * DSTT settings tab (title: DSTT). Renders the four-way mode picker plus
1981
- * a live status line. Reads/writes the durable namespace through the
1982
- * host-side RPC mirror; every subscription and timer is owned by the
1983
- * component.
1984
- */
1985
- function DsttSection(props) {
1986
- const dstt = props.dstt;
1987
- const locale = props.locale;
1988
- const [mode, setMode] = React.useState(() => (dstt === undefined ? 'peakvalley-redblue' : dstt.mode()));
1989
- const [active, setActive] = React.useState('zh');
1990
- const [, force] = React.useReducer((x) => x + 1, 0);
1991
-
1992
- React.useEffect(() => {
1993
- if (locale === undefined) return;
1994
- const sync = () => {
1995
- try { setActive(locale.getLocale().active); } catch (e) {}
1996
- };
1997
- sync();
1998
- return locale.subscribe(sync);
1999
- }, [locale]);
2000
-
2001
- React.useEffect(() => {
2002
- if (dstt === undefined) return () => {};
2003
- const refresh = () => {
2004
- try { setMode(dstt.mode()); } catch (e) {}
2005
- force();
2006
- };
2007
- let unsubscribe = null;
2008
- try { unsubscribe = dstt.subscribe(refresh); } catch (e) { unsubscribe = null; }
2009
- const unsubscribeColor = subscribeColor(() => force());
2010
- refresh();
2011
- const timer = setInterval(() => {
2012
- force();
2013
- }, 30000);
2014
- return () => {
2015
- clearInterval(timer);
2016
- if (unsubscribe !== null) unsubscribe();
2017
- unsubscribeColor();
2018
- };
2019
- }, [dstt]);
2020
-
2021
- const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
2022
- const nowMin = ceilToMinute(beijingNow());
2023
- const peakNow = isPeakMinute(nowMin);
2024
- const end = peakNow ? endOfWindow(nowMin) : null;
2025
- const deltaMins = end === null ? 0 : Math.max(0, Math.round((end.getTime() - nowMin.getTime()) / 60000));
2026
- const deltaText = deltaMins <= 0
2027
- ? ''
2028
- : isZh
2029
- ? (deltaMins >= 60 ? Math.floor(deltaMins / 60) + '小时' + String(deltaMins % 60).padStart(2, '0') + '分' : deltaMins + '分')
2030
- : (deltaMins >= 60 ? Math.floor(deltaMins / 60) + 'h ' + (deltaMins % 60) + 'm' : deltaMins + 'm');
2031
- const colorNow = document.body.getAttribute('data-dshome-color') || 'green';
2032
- const colorName = colorNow === 'red' ? (isZh ? '鲜红' : 'vivid red') : colorNow === 'blue' ? (isZh ? '蓝色' : 'blue') : (isZh ? '绿色' : 'green');
2033
- const timeSliced = isTimeSliced(mode);
2034
- const status = mode === 'always-blue'
2035
- ? (isZh ? '固定蓝色模式' : 'Fixed blue')
2036
- : mode === 'always-green'
2037
- ? (isZh ? '固定绿色模式' : 'Fixed green')
2038
- : peakNow
2039
- ? (isZh ? '高峰时段 · 鲜红' + (deltaText === '' ? '' : ' · 还剩 ' + deltaText) : 'Peak hours · vivid red' + (deltaText === '' ? '' : ' · ' + deltaText + ' left'))
2040
- : (isZh ? '谷时段 · ' + (mode === 'peakvalley-redgreen' ? '绿色' : '蓝色') : 'Off-peak · ' + (mode === 'peakvalley-redgreen' ? 'green' : 'blue'));
2041
-
2042
- const pickMode = (next) => {
2043
- if (dstt === undefined || next === mode) return;
2044
- dstt.set(next);
2045
- };
2046
-
2047
- return React.createElement('div', { className: 'dshome-dstt' },
2048
- React.createElement('h3', null,
2049
- 'DeepSeekStyleTheme',
2050
- React.createElement('span', { className: 'dshome-dstt-tag' }, 'DSTT')),
2051
- React.createElement('p', null, isZh
2052
- ? '谷时段默认绿色;高峰时段(北京时间 9:00–12:00、14:00–18:00,周六周日全天非高峰)自动切换为鲜红提示。峰谷模式可选谷色为蓝或绿,常态模式全天固定。'
2053
- : 'Off-peak green; peak hours (Beijing 9:00–12:00, 14:00–18:00, all day off-peak on weekends) switch to vivid red. Peak/valley modes pick the valley color (blue or green); always modes stay fixed all day.'),
2054
- React.createElement('div', { className: 'dshome-dstt-row' },
2055
- React.createElement('div', { className: 'dshome-dstt-row-body' },
2056
- React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '主题模式' : 'Theme mode'),
2057
- React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
2058
- ? '峰谷红蓝 / 峰谷红绿:高峰鲜红、谷时段蓝 / 绿;常态绿 / 常态蓝:全天固定'
2059
- : 'Peak R·B / Peak R·G: red at peak, blue/green at valley; Always G/B: fixed all day')),
2060
- React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '主题模式' : 'Theme mode' },
2061
- DSTT_MODES.map((m) => React.createElement('button', {
2062
- key: m,
2063
- type: 'button',
2064
- role: 'radio',
2065
- 'aria-checked': mode === m,
2066
- className: 'dshome-dstt-seg-btn' + (mode === m ? ' dshome-on' : ''),
2067
- onClick: () => pickMode(m)
2068
- }, isZh ? DSTT_LABELS[m].zh : DSTT_LABELS[m].en)))),
2069
- React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
2070
- '当前:' + colorName + ' · ' + status));
2071
- }
2072
-
2073
- function apply(ctx) {
2074
- // A `document.querySelector('[data-dsh-probe]')` cleanup used to run
2075
- // here. Removing an element selected by a generic attribute deletes
2076
- // whatever else happens to use that attribute, so it is gone: a probe
2077
- // owns the nodes it creates and removes them itself.
2078
-
2079
- // Core styles (ours only) are always injected.
2080
- ctx.effect(() => injectStyle(CORE_CSS, 'theme.css'), 'deepseek-style-theme: core css');
2081
- ctx.effect(() => injectStyle(ACTIONS_CSS, 'actions.css'), 'deepseek-style-theme: actions css');
2082
-
2083
- const theme = ctx.get('theme');
2084
-
2085
- // DSTT: the durable mode travels over this plugin's private loopback
2086
- // channel (the settings RPC surface only serves allowlisted
2087
- // namespaces). Pull it once, then let the color driver and the
2088
- // settings tab share the in-memory mirror.
2089
- ctx.effect(() => {
2090
- dsttSync(ctx);
2091
- }, 'deepseek-style-theme: dstt sync');
2092
- ctx.effect(() => watchPeakHour(ctx), 'deepseek-style-theme: color driver');
2093
-
2094
- // Dark marker: owned attribute driven by the theme service, never by
2095
- // the product's attribute name. Must precede the patch blocks so the
2096
- // dark selectors match from the first paint.
2097
- ctx.effect(() => createDarkSync(ctx, theme), 'deepseek-style-theme: dark sync');
2098
-
2099
- // Guarded product-surface patches: each block activates only while
2100
- // its anchor class exists in the DOM.
2101
- for (const block of PATCH_BLOCKS) {
2102
- ctx.effect(() => installGuarded(block.anchor, block.css), 'deepseek-style-theme: patch ' + block.anchor);
2103
- }
2104
-
2105
- if (theme !== undefined) {
2106
- const disposeTokens = theme.overrideTokens('deepseek-style-theme', {
2107
- '--dsw-alias-bg-base': { light: '#f6f7fb', dark: '#0a1a3a' },
2108
- '--dsw-alias-bg-layer-1': { light: 'rgba(255,255,255,0.72)', dark: 'rgba(15,32,66,0.72)' },
2109
- '--dsw-alias-bg-layer-2': { light: 'rgba(255,255,255,0.85)', dark: 'rgba(21,42,86,0.8)' },
2110
- '--dsw-alias-bg-overlay': { light: 'rgba(255,255,255,0.92)', dark: 'rgba(10,26,58,0.94)' },
2111
- '--dsw-alias-border-l1': { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.08)' },
2112
- '--dsw-alias-border-l2': { light: 'rgba(0,0,0,0.1)', dark: 'rgba(255,255,255,0.14)' },
2113
- '--dsw-alias-brand-primary': { light: 'var(--ds-brand)', dark: 'color-mix(in srgb,var(--ds-brand) 72%,#fff)' },
2114
- '--dsw-alias-label-primary': { light: '#152443', dark: '#eef2ff' },
2115
- '--dsw-alias-label-secondary': { light: '#3c4a63', dark: '#a9b8d8' },
2116
- '--dsw-alias-state-error-primary': { light: '#ef4444', dark: '#ff6b6b' },
2117
- '--dsw-alias-state-success-primary': { light: '#22c55e', dark: '#4ed17e' },
2118
- '--dsw-alias-state-warn-primary': { light: '#f59e0b', dark: '#f7ad31' },
2119
- '--dsw-specific-sidebar-fill': { light: 'rgba(255,255,255,0.5)', dark: 'rgba(10,26,58,0.48)' }
2120
- });
2121
- ctx.effect(() => disposeTokens);
2122
- }
2123
-
2124
- ctx.effect(() => {
2125
- try { return startParticles(); } catch (error) { return () => {}; }
2126
- });
2127
- ctx.effect(() => {
2128
- try { return watchSettingsSection(); } catch (error) { return () => {}; }
2129
- });
2130
- ctx.effect(() => {
2131
- try { return watchBrandLink(); } catch (error) { return () => {}; }
2132
- });
2133
- ctx.effect(() => {
2134
- try { return watchHeaderInteraction(); } catch (error) { return () => {}; }
2135
- });
2136
- ctx.effect(() => {
2137
- try { return watchWorkspaceMenu(ctx); } catch (error) { return () => {}; }
2138
- }, 'deepseek-style-theme: workspace open menu');
2139
- ctx.effect(() => {
2140
- try { return watchFileCardMenu(ctx); } catch (error) { return () => {}; }
2141
- }, 'deepseek-style-theme: delivered file card menu');
2142
- ctx.effect(() => {
2143
- try { return watchRunningSubagents(ctx); } catch (error) { return () => {}; }
2144
- }, 'deepseek-style-theme: running subagents panel');
2145
-
2146
- const slots = ctx.get('slots');
2147
- if (slots !== undefined) {
2148
- slots.inject('conversation.session.header.utilities', () => slots.register(
2149
- { name: 'conversation.session.header.utilities', id: 'deepseek-locale', order: 90 },
2150
- () => React.createElement(LocaleToggle, { locale: ctx.get('locale') })
2151
- ));
2152
- slots.inject('settings.section', () => slots.register({
2153
- name: 'settings.section',
2154
- id: 'dstt',
2155
- order: 100,
2156
- label: () => 'DSTT',
2157
- inject: () => ({
2158
- dstt: {
2159
- mode: dsttGetMode,
2160
- set: (mode) => dsttSetMode(ctx, mode),
2161
- subscribe: dsttSubscribe
2162
- },
2163
- locale: ctx.get('locale')
2164
- })
2165
- }, DsttSection));
2166
- }
2167
- }
2168
-
2169
- exports.apply = apply;
2170
- exports.inject = ['theme', 'slots', 'locale', 'workspaces', 'connection'];
2171
- return module.exports;
2172
- }
2173
- });
1
+ const dshDeepseekStyleThemeFactory = (require) => {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
5
+ const React = require("react");
6
+
7
+ // ── design-standard conformance (创造模式 preset) ────────────────────
8
+ // The product shell ships build-time hashed class names (e.g.
9
+ // `wSkVaW_header`). They are not API and change on every product rebuild,
10
+ // so this theme treats every rule that targets them as a guarded patch
11
+ // layer: each block is injected only while its anchor class actually
12
+ // exists in the DOM (a 30s wait covers late-rendered shells), and a block
13
+ // whose anchor never appears is skipped with a one-line diagnostic
14
+ // instead of silently shipping dead CSS.
15
+ // Dark styling keys off our own `data-dshome-dark` attribute, driven by
16
+ // the theme service's `active.colorScheme` — never the product's
17
+ // attribute name — so a product-side rename cannot break dark mode.
18
+ // UI that the product exposes through a Slot (the locale toggle) stays on
19
+ // the Slot; only surfaces with no Slot use DOM patches.
20
+ //
21
+ // Markup sinks: `innerHTML` appears exactly five times in this file and
22
+ // every one of them assigns a module-level or call-site `const` SVG
23
+ // literal. It is this file's only markup sink, so anything derived from
24
+ // page data — session titles, file paths, workspace names — must go
25
+ // through `textContent`/`title`/`setAttribute`, never `innerHTML`. An
26
+ // interpolated value there would be DOM XSS.
27
+ const PLUGIN_ID = "dsh-deepseek-style-theme";
28
+
29
+ // CORE_CSS: ours only (`:root` tokens, `html/body`, keyframes, stable
30
+ // `[role=menu]`, `.dshome-*`). No product class names — always injected.
31
+ // Color model: `data-dshome-color` ∈ {green(base), red(鲜红 peak), blue}
32
+ // re-tints every var(--ds-brand*) consumer.
33
+ const CORE_CSS = `
34
+ :root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
35
+ html,body{background-color:transparent!important}
36
+ body{background:radial-gradient(1200px 700px at 12% -10%,color-mix(in srgb,var(--ds-brand) 14%,transparent),transparent 60%),radial-gradient(1000px 600px at 105% 12%,color-mix(in srgb,var(--ds-brand-light) 16%,transparent),transparent 55%),radial-gradient(900px 700px at 50% 115%,color-mix(in srgb,var(--ds-brand) 10%,transparent),transparent 60%),linear-gradient(180deg,#fbfbfd 0%,#f5f6fa 100%);background-attachment:fixed}
37
+ body[data-dshome-color=red]{--ds-brand:#F5222D;--ds-brand-deep:#CF1322;--ds-brand-mid:#FF4D4F;--ds-brand-light:#FF7875}
38
+ body[data-dshome-color=blue]{--ds-brand:#4D6BFE;--ds-brand-deep:#3A65C2;--ds-brand-mid:#4176E6;--ds-brand-light:#73A3D2}
39
+ body[data-dshome-dark]{--ds-brand:#10B981;--ds-brand-deep:#059669;--ds-brand-mid:#34D399;--ds-brand-light:#6EE7B7;background:radial-gradient(1100px 650px at 8% -8%,#0a3a28 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#0d4d35 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#06291c 0,transparent 60%),linear-gradient(180deg,#081b14 0%,#0a241a 55%,#081b14 100%);background-attachment:fixed}
40
+ body[data-dshome-dark][data-dshome-color=red]{--ds-brand:#FF4D4F;--ds-brand-deep:#F5222D;--ds-brand-mid:#FF7875;--ds-brand-light:#FFA39E;background:radial-gradient(1100px 650px at 8% -8%,#5c0f14 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#7d1a20 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#3f0a0e 0,transparent 60%),linear-gradient(180deg,#260709 0%,#330a0d 55%,#260709 100%);background-attachment:fixed}
41
+ body[data-dshome-dark][data-dshome-color=blue]{--ds-brand:#5D79FF;--ds-brand-deep:#4D6BFE;--ds-brand-mid:#7A96FF;--ds-brand-light:#9DB9FF;background:radial-gradient(1100px 650px at 8% -8%,#1a3870 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#2d5f9e 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#0d1f4a 0,transparent 60%),linear-gradient(180deg,#0a1a3a 0%,#0d1f4a 55%,#0a1a3a 100%);background-attachment:fixed}
42
+ @property --dsh-border-angle{syntax:"<angle>";initial-value:0deg;inherits:false}
43
+ @keyframes dsh-border-spin{to{--dsh-border-angle:360deg}}
44
+ @keyframes dshome-view-enter{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
45
+ @keyframes dshome-pop-in{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:none}}
46
+ @keyframes dshome-fade-in{from{opacity:0}to{opacity:1}}
47
+ @keyframes dshome-tab-in{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
48
+ @keyframes dshome-modal-in{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:none}}
49
+ [role=menu]{animation:dshome-pop-in .22s cubic-bezier(.2,.8,.2,1)}
50
+ .dshome-ws-open-wrap{padding:2px 0}
51
+ .dshome-ws-open{display:flex;align-items:center;gap:8px;width:100%;min-width:170px;padding:6px 10px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease}
52
+ .dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)!important;color:var(--ds-brand-deep)!important}
53
+ .dshome-ws-open-icon{display:inline-flex;flex:none;color:var(--ds-brand)}
54
+ .dshome-ws-open-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
55
+ body[data-dshome-dark] .dshome-ws-open{color:#eef2ff}
56
+ body[data-dshome-dark] .dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent)!important;color:#fff!important}
57
+ body[data-dshome-dark] .dshome-ws-open-icon{color:var(--ds-brand-light)}
58
+ .dshome-dstt{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}
59
+ .dshome-dstt h3{margin:0;font-size:16px;font-weight:500;line-height:24px;display:flex;align-items:center;gap:8px}
60
+ .dshome-dstt-tag{flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand);border-radius:999px;padding:1px 8px;font-size:11px;line-height:18px;font-weight:600}
61
+ body[data-dshome-dark] .dshome-dstt-tag{color:var(--ds-brand-light)}
62
+ .dshome-dstt p{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
63
+ .dshome-dstt-row{display:flex;align-items:center;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;background:var(--dsw-alias-bg-layer-1)}
64
+ .dshome-dstt-row-body{flex:1;min-width:0}
65
+ .dshome-dstt-row-title{font-size:14px;line-height:22px;font-weight:500}
66
+ .dshome-dstt-row-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
67
+ .dshome-dstt-status{flex:none;font-size:12px;line-height:18px;padding:4px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
68
+ .dshome-dstt-status[data-peak=true]{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep);border:1px solid color-mix(in srgb,var(--ds-brand) 30%,transparent)}
69
+ .dshome-dstt-status[data-peak=false]{background:color-mix(in srgb,var(--ds-brand) 8%,transparent);color:var(--ds-brand);border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent)}
70
+ body[data-dshome-dark] .dshome-dstt-status[data-peak=true]{color:var(--ds-brand-light)}
71
+ body[data-dshome-dark] .dshome-dstt-status[data-peak=false]{color:var(--ds-brand-light)}
72
+ .dshome-dstt-seg{display:inline-flex;align-items:center;gap:2px;flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);padding:2px}
73
+ .dshome-dstt-seg-btn{height:26px;padding:0 12px;border:none;border-radius:999px;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:26px;transition:background .15s ease,color .15s ease}
74
+ .dshome-dstt-seg-btn:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)}
75
+ .dshome-dstt-seg-btn.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff}
76
+ .dshome-dstt-seg-btn:disabled{opacity:.5;cursor:default}
77
+ .dshome-dstt-seg-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
78
+ `;
79
+
80
+ // Locale toggle styling. The two header-hover dependent rules live in the
81
+ // `.wSkVaW_root` patch block below, so the toggle stays styled (short
82
+ // labels) even if the header class renames.
83
+ // The product sets `corner-shape:superellipse(1.5)` on `*` (its
84
+ // corner-shape.css), which flattens even a 999px radius into a rounded
85
+ // rectangle; `corner-shape:round` restores true round ends here.
86
+ // Label bloom: the short 中/E label grows into 中文/EN with an animated
87
+ // max-width instead of the old `display` hard swap.
88
+ const ACTIONS_CSS = `
89
+ .dshome-locale{display:inline-flex;align-items:center;height:28px;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);overflow:hidden;flex:none;corner-shape:round;transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
90
+ .dshome-locale:hover{transform:translateY(-1px) scale(1.03);border-color:color-mix(in srgb,var(--ds-brand) 48%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 26%,transparent)}
91
+ body[data-dshome-dark] .dshome-locale{border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent)}
92
+ body[data-dshome-dark] .dshome-locale:hover{border-color:color-mix(in srgb,var(--ds-brand) 55%,transparent);background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
93
+ .dshome-locale button{height:28px;padding:0 11px;border:none;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:28px;transition:background .22s ease,color .22s ease,transform .18s cubic-bezier(.2,.8,.2,1)}
94
+ .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep)}
95
+ .dshome-locale button:active{transform:scale(.92)}
96
+ .dshome-locale button.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;animation:dshome-locale-pop .34s cubic-bezier(.2,.8,.2,1)}
97
+ @keyframes dshome-locale-pop{0%{transform:scale(.86)}60%{transform:scale(1.05)}to{transform:none}}
98
+ .dshome-locale .dshome-short,.dshome-locale .dshome-full{display:inline-block;overflow:hidden;white-space:nowrap;transition:max-width .3s cubic-bezier(.2,.8,.2,1),opacity .22s ease,transform .3s cubic-bezier(.2,.8,.2,1)}
99
+ .dshome-locale .dshome-short{max-width:2.2em;opacity:1;transform:none}
100
+ .dshome-locale .dshome-full{max-width:0;opacity:0;transform:translateX(-6px)}
101
+ body[data-dshome-dark] .dshome-locale button{color:var(--ds-brand-light)}
102
+ body[data-dshome-dark] .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent);color:#fff}
103
+ body[data-dshome-dark] .dshome-locale button.dshome-on{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff),var(--ds-brand));color:#fff}
104
+ `;
105
+
106
+ const SUBAGENT_PANEL_CSS = `
107
+ .dshome-sa{position:fixed;right:16px;bottom:16px;z-index:70;width:286px;max-width:calc(100vw - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px;border-radius:16px;border:1px solid rgba(255,255,255,.55);background:linear-gradient(165deg,rgba(255,255,255,.46),rgba(255,255,255,.22));box-shadow:0 12px 40px rgba(13,30,60,.14),inset 0 1px 0 rgba(255,255,255,.85),inset 0 0 0 .5px rgba(255,255,255,.5);-webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);font-family:inherit;transition:opacity .22s ease,transform .22s ease;animation:dshome-sa-in .3s cubic-bezier(.2,.8,.2,1)}
108
+ .dshome-sa[hidden]{display:none!important}
109
+ .dshome-sa.dshome-sa-leave{opacity:0;transform:translateY(10px);pointer-events:none}
110
+ body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(21,42,86,.52),rgba(10,26,58,.3));border-color:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 12px 40px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 .5px color-mix(in srgb,var(--ds-brand) 16%,transparent)}
111
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.dshome-sa{background:linear-gradient(165deg,rgba(255,255,255,.84),rgba(255,255,255,.68))}body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(13,30,60,.88),rgba(10,26,58,.78))}}
112
+ @keyframes dshome-sa-in{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
113
+ .dshome-sa-head{display:flex;flex-direction:row;align-items:center;gap:7px;min-height:26px}
114
+ .dshome-sa-pulse{position:relative;flex:none;width:8px;height:8px;border-radius:50%;background:var(--ds-brand);animation:dshome-sa-pulse 1.8s ease-out infinite}
115
+ @keyframes dshome-sa-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 45%,transparent)}70%{box-shadow:0 0 0 7px color-mix(in srgb,var(--ds-brand) 0%,transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 0%,transparent)}}
116
+ .dshome-sa-title{flex:1;min-width:0;font-size:12.5px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
117
+ .dshome-sa-collapse{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:50%;background:rgba(255,255,255,.5);color:var(--ds-brand);cursor:pointer;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 18%,transparent);transition:transform .2s ease,background .15s ease}
118
+ .dshome-sa-collapse:hover{background:rgba(255,255,255,.75)}
119
+ body[data-dshome-dark] .dshome-sa-collapse{background:color-mix(in srgb,var(--ds-brand) 18%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 25%,transparent)}
120
+ body[data-dshome-dark] .dshome-sa-collapse:hover{background:color-mix(in srgb,var(--ds-brand) 30%,transparent)}
121
+ .dshome-sa-collapse svg{display:block}
122
+ .dshome-sa-collapsed .dshome-sa-collapse{transform:rotate(180deg)}
123
+ .dshome-sa-list{display:flex;flex-direction:column;gap:2px;max-height:312px;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;padding:2px 0}
124
+ .dshome-sa-collapsed .dshome-sa-list{display:none}
125
+ .dshome-sa-item{position:relative;display:flex;flex-direction:row;align-items:flex-start;gap:8px;width:100%;min-width:0;box-sizing:border-box;padding:7px 8px;border:none;border-radius:10px;background:transparent;color:inherit;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease}
126
+ .dshome-sa-item:hover{background:linear-gradient(135deg,rgba(255,255,255,.65),rgba(255,255,255,.35));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 14%,transparent)}
127
+ body[data-dshome-dark] .dshome-sa-item:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 22%,transparent),color-mix(in srgb,var(--ds-brand) 10%,transparent));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
128
+ .dshome-sa-dot{flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-light));animation:dshome-sa-blink 1.2s ease-in-out infinite}
129
+ @keyframes dshome-sa-blink{0%,100%{opacity:1}50%{opacity:.35}}
130
+ .dshome-sa-stop{flex:none;width:18px;height:18px;margin-top:4px;padding:0;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;pointer-events:none;transition:opacity .15s ease,background .15s ease,color .15s ease}
131
+ .dshome-sa-item:hover .dshome-sa-stop,.dshome-sa-item:focus-within .dshome-sa-stop{opacity:1;pointer-events:auto}
132
+ .dshome-sa-stop:hover{background:rgba(239,68,68,.12);color:#ef4444}
133
+ body[data-dshome-dark] .dshome-sa-stop:hover{background:rgba(255,107,107,.16);color:#ff6b6b}
134
+ .dshome-sa-stop:disabled{opacity:.55;cursor:default}
135
+ .dshome-sa-stop-busy svg{animation:dshome-sa-blink .8s ease-in-out infinite}
136
+ .dshome-sa-stop-denied{background:rgba(239,68,68,.18)!important;color:#ef4444!important}
137
+ body[data-dshome-dark] .dshome-sa-stop-denied{background:rgba(255,107,107,.2)!important;color:#ff6b6b!important}
138
+ .dshome-sa-sweep{position:absolute;top:0;bottom:0;left:0;width:0;border-radius:10px;background:linear-gradient(90deg,rgba(239,68,68,.14),rgba(239,68,68,.24));pointer-events:none;z-index:0}
139
+ body[data-dshome-dark] .dshome-sa-sweep{background:linear-gradient(90deg,rgba(255,107,107,.16),rgba(255,107,107,.28))}
140
+ .dshome-sa-item>.dshome-sa-dot,.dshome-sa-item>.dshome-sa-body,.dshome-sa-item>.dshome-sa-stop{position:relative;z-index:1}
141
+ .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(239,68,68,.85)}
142
+ .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar-fill{display:none}
143
+ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(255,107,107,.8)}
144
+ .dshome-sa-item.dshome-sa-stopping .dshome-sa-stop{opacity:0;pointer-events:none}
145
+ .dshome-sa-item.dshome-sa-item-exit{opacity:0;transform:translateX(8px);transition:opacity .2s ease,transform .2s ease}
146
+ .dshome-sa-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
147
+ .dshome-sa-name{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
148
+ .dshome-sa-meta{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
149
+ .dshome-sa-bar{position:relative;height:3px;border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 12%,transparent);overflow:hidden}
150
+ .dshome-sa-bar-fill{position:absolute;inset:0;width:42%;border-radius:999px;background:linear-gradient(90deg,transparent,var(--ds-brand),transparent);animation:dshome-sa-slide 1.5s ease-in-out infinite}
151
+ @keyframes dshome-sa-slide{0%{transform:translateX(-110%)}100%{transform:translateX(350%)}}
152
+ @media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
153
+ `;
154
+
155
+ // PATCH_BLOCKS: every rule group that targets product build-time class
156
+ // names. `anchor` is the class that must exist in the DOM before the
157
+ // block is injected; each block degrades independently when its surface
158
+ // renames in a product rebuild.
159
+ const PATCH_BLOCKS = [
160
+ // Frame / sidebar / details columns.
161
+ { anchor: ".pI_x6G_frame", css: `
162
+ .pI_x6G_frame,.pI_x6G_sidebarCol,.pI_x6G_detailsCol,.pI_x6G_centerCol{background:transparent!important}
163
+ .pI_x6G_sidebarCol{border-right:none!important}
164
+ .pI_x6G_detailsCol{border-left:none!important}
165
+ ` },
166
+ // Sidebar glass root — Apple liquid glass pane: layered translucent
167
+ // gradient + blur/saturate/brightness/contrast + specular edges.
168
+ { anchor: ".hHd-Xa_root", css: `
169
+ .hHd-Xa_root{position:relative;background:transparent!important}
170
+ .hHd-Xa_root::before{content:"";position:absolute;inset:0;z-index:-1;background:rgba(255,255,255,.5);-webkit-backdrop-filter:blur(var(--ds-glass-blur));backdrop-filter:blur(var(--ds-glass-blur));border-right:1px solid rgba(0,0,0,.06);pointer-events:none}
171
+ body[data-dshome-dark] .hHd-Xa_root::before{background:rgba(10,26,58,.48);border-right:1px solid rgba(255,255,255,.08)}
172
+ .hHd-Xa_brand{color:var(--ds-brand);cursor:pointer}
173
+ body[data-dshome-dark] .hHd-Xa_brand{color:#fff}
174
+ .hHd-Xa_newSession{border:none!important;background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;color:#fff!important;border-radius:999px!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:transform .15s ease,box-shadow .15s ease,background .15s ease}
175
+ .hHd-Xa_newSession:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff) 0%,var(--ds-brand) 100%)!important;box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
176
+ ` },
177
+ // Session / project rows and icon buttons.
178
+ { anchor: ".YDXeBa_sessionRow", css: `
179
+ .YDXeBa_sessionRow,.YDXeBa_projectRow{background:rgba(255,255,255,.35)!important;border:1px solid transparent;border-radius:10px!important;margin:1px 0;transition:background .15s ease,border-color .15s ease,transform .15s ease}
180
+ .YDXeBa_sessionRow:hover,.YDXeBa_projectRow:hover{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transform:translateX(2px)}
181
+ .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 16%,transparent),color-mix(in srgb,var(--ds-brand-mid) 12%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 25%,transparent);color:var(--ds-brand-deep)}
182
+ body[data-dshome-dark] .YDXeBa_sessionRow,body[data-dshome-dark] .YDXeBa_projectRow{background:rgba(21,42,86,.4)!important}
183
+ body[data-dshome-dark] .YDXeBa_sessionRow:hover,body[data-dshome-dark] .YDXeBa_projectRow:hover{background:rgba(30,52,100,.6)!important}
184
+ body[data-dshome-dark] .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 28%,transparent),color-mix(in srgb,var(--ds-brand) 20%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);color:#eef2ff}
185
+ .YDXeBa_iconButton{transition:background .15s ease,color .15s ease,transform .15s ease,opacity .15s ease}
186
+ .YDXeBa_iconButton:hover{transform:scale(1.1)}
187
+ ` },
188
+ // Chat view: header (incl. locale hover swap), tabs, crumbs, title.
189
+ // Hover pill upgraded to a liquid-glass capsule.
190
+ // 0.1.5-rc.1 draws the header divider as a real `border-bottom` on
191
+ // `.wSkVaW_header` (1.37 used an `::after` pseudo-element), so the
192
+ // floating capsule grew a stray hairline under it. Neutralise the
193
+ // border here and keep the legacy `::after` hide for older builds.
194
+ // The header divider would otherwise be a 0.5px `--dsw-alias-border-l3`
195
+ // line spanning exactly this absolutely positioned bar (24px inset).
196
+ // `titleRow`/`titleCluster` are flattened with `display:contents`, so
197
+ // every direct header child needs an explicit `order`; 0.1.5-rc.1 added
198
+ // `headerCorner` (the right-sidebar expand button) and, left at the
199
+ // default `order:0`, it tied with the tabs and landed at the far left,
200
+ // where the first tab pill overlapped it. It now trails the utilities
201
+ // block, i.e. sits right of the 中/EN locale toggle.
202
+ { anchor: ".wSkVaW_root", css: `
203
+ .wSkVaW_root{position:relative;background:transparent!important}
204
+ .wSkVaW_scrollBody{padding-top:56px}
205
+ .wSkVaW_viewArea{padding:0 18px}
206
+ .wSkVaW_viewArea{animation:dshome-view-enter .45s ease}
207
+ .wSkVaW_header{position:absolute;top:8px;left:24px;right:24px;z-index:5;min-height:36px;padding:4px 12px;display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-start;border:0!important;border-radius:0;background:transparent!important;transition:border-radius .3s ease}
208
+ .wSkVaW_header::before{content:"";position:absolute;inset:0;z-index:-1;background:transparent;border:1px solid transparent;border-radius:inherit;-webkit-backdrop-filter:none;backdrop-filter:none;transition:background .3s ease,border-color .3s ease,box-shadow .3s ease}
209
+ .wSkVaW_header:hover{border-radius:999px}
210
+ .wSkVaW_titleRow{display:contents}
211
+ .wSkVaW_titleCluster{display:contents}
212
+ .wSkVaW_crumbs{order:2;flex:none;min-width:0}
213
+ .wSkVaW_tabs{order:0;flex:none;align-items:center;display:none;gap:8px;margin:0;padding:0}
214
+ .wSkVaW_headerActions{order:1;flex:none;align-items:center;display:flex;gap:8px}
215
+ .wSkVaW_headerUtilities{order:3;flex:none;align-items:center;display:flex;gap:8px;margin-left:auto}
216
+ .wSkVaW_headerCorner{order:4;flex:none;align-items:center;display:flex;margin-left:0;margin-right:0}
217
+ .wSkVaW_header.dshome-swap .wSkVaW_tabs{display:flex;animation:dshome-tab-in .22s ease}
218
+ .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_tabs{display:flex!important}
219
+ .wSkVaW_header:hover::before{background:rgba(255,255,255,.45);border-color:rgba(0,0,0,.06);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 4px 16px rgba(0,0,0,.05)}
220
+ body[data-dshome-dark] .wSkVaW_header:hover::before{background:rgba(13,30,60,.45);border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.3)}
221
+ .wSkVaW_header:after{display:none!important}
222
+ .SVAs4q_label{background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent);border-radius:999px!important;padding:0 10px!important;height:28px;line-height:26px;color:var(--ds-brand)!important}
223
+ body[data-dshome-dark] .SVAs4q_label{color:var(--ds-brand-light)!important}
224
+ .wSkVaW_tab{border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);border-radius:999px;corner-shape:round;padding:5px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:16px;font-weight:500;transition:color .22s ease,background .22s ease,border-color .22s ease,box-shadow .24s ease,transform .24s cubic-bezier(.2,.8,.2,1)}
225
+ .wSkVaW_tab:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);border-color:color-mix(in srgb,var(--ds-brand) 42%,transparent);box-shadow:0 4px 12px color-mix(in srgb,var(--ds-brand) 22%,transparent);transform:translateY(-1px)}
226
+ .wSkVaW_tab:active{transform:translateY(0) scale(.96)}
227
+ .wSkVaW_tab::after{display:none!important}
228
+ .wSkVaW_tabActive{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;border-color:transparent;box-shadow:0 3px 10px color-mix(in srgb,var(--ds-brand) 26%,transparent);animation:dshome-tab-pop .34s cubic-bezier(.2,.8,.2,1)}
229
+ .wSkVaW_tabActive:hover{color:#fff}
230
+ body[data-dshome-dark] .wSkVaW_tab{border-color:color-mix(in srgb,var(--ds-brand) 32%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent)}
231
+ body[data-dshome-dark] .wSkVaW_tab:hover{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
232
+ body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb,var(--ds-brand) 40%,transparent)}
233
+ @keyframes dshome-tab-pop{0%{transform:translateY(-1px) scale(.88)}60%{transform:translateY(-1px) scale(1.05)}to{transform:none}}
234
+ .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover{border-radius:0}
235
+ .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover::before{background:transparent;border-color:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
236
+ .wSkVaW_composerSeat{background:transparent!important}
237
+ .wSkVaW_header:hover .dshome-locale .dshome-short{max-width:0;opacity:0;transform:translateX(6px)}
238
+ .wSkVaW_header:hover .dshome-locale .dshome-full{max-width:3em;opacity:1;transform:none}
239
+ /* Header chips: the product's global corner-shape:superellipse(1.5) squares off
240
+ even pill radii, so every control in the bar is restored to a true round end
241
+ (and the session-title crumb, 12px on a 28px box, becomes a real pill). */
242
+ .wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header .nL4_yW_sessionLogButton,.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
243
+ .wSkVaW_header .wSkVaW_crumb{border-radius:999px}
244
+ /* Open-in-app split button: hover lift with a brand tint, and the chevron turns
245
+ over while its menu is open. */
246
+ .wSkVaW_header .CAgGvG_split{transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
247
+ .wSkVaW_header .CAgGvG_split:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ds-brand) 40%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
248
+ .wSkVaW_header .CAgGvG_split:active{transform:translateY(0) scale(.97)}
249
+ .wSkVaW_header .CAgGvG_main,.wSkVaW_header .CAgGvG_chevron{transition:background .22s ease,color .22s ease}
250
+ .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,.wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand-deep)}
251
+ .wSkVaW_header .CAgGvG_chevron svg{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
252
+ .wSkVaW_header .CAgGvG_chevron[aria-expanded=true] svg{transform:rotate(180deg)}
253
+ body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover{border-color:color-mix(in srgb,var(--ds-brand) 52%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 30%,transparent)}
254
+ body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);color:#fff}
255
+ ` },
256
+ // Session-log button (hover expand).
257
+ { anchor: ".nL4_yW_sessionLogButton", css: `
258
+ .nL4_yW_sessionLogButton{min-width:0!important;width:28px;height:28px;padding:0!important;border-radius:50%!important;border:none!important;background:color-mix(in srgb,var(--ds-brand) 8%,transparent)!important;color:var(--ds-brand)!important;overflow:hidden;transition:width .2s ease,padding .2s ease,border-radius .2s ease}
259
+ .nL4_yW_sessionLogButton span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
260
+ .wSkVaW_header:hover .nL4_yW_sessionLogButton{width:auto;padding:0 12px!important;border-radius:18px!important}
261
+ .wSkVaW_header:hover .nL4_yW_sessionLogButton span{max-width:90px;opacity:1}
262
+ body[data-dshome-dark] .nL4_yW_sessionLogButton{color:var(--ds-brand-light)!important}
263
+ ` },
264
+ // Glass cards with spinning gradient border — upgraded to liquid glass:
265
+ // specular top edge, saturated blur, brighter inner rim.
266
+ { anchor: ".uV2eYG_card", css: `
267
+ .uV2eYG_card{position:relative;background:rgba(255,255,255,.42)!important;border:1.5px solid rgba(255,255,255,.55)!important;box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8);border-radius:22px!important}
268
+ body[data-dshome-dark] .uV2eYG_card{background:rgba(13,30,60,.55)!important;border:1.5px solid rgba(255,255,255,.16)!important;box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
269
+ .uV2eYG_card::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:22px;-webkit-backdrop-filter:blur(28px);backdrop-filter:blur(28px);pointer-events:none}
270
+ .uV2eYG_card::before{content:"";position:absolute;inset:-1px;border-radius:inherit;padding:1px;background:conic-gradient(from var(--dsh-border-angle),color-mix(in srgb,var(--ds-brand) 0%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand-light) 20%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand) 0%,transparent));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;pointer-events:none;animation:dsh-border-spin 6s linear infinite;z-index:0}
271
+ body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(--dsh-border-angle),rgba(255,255,255,0),color-mix(in srgb,var(--ds-brand) 55%,transparent),color-mix(in srgb,var(--ds-brand-light) 25%,transparent),color-mix(in srgb,var(--ds-brand) 55%,transparent),rgba(255,255,255,0))}
272
+ .uV2eYG_card>*{position:relative;z-index:1}
273
+ .uV2eYG_primary{background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;border:none!important;color:#fff!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:box-shadow .15s ease,transform .15s ease}
274
+ .uV2eYG_primary:hover{box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
275
+ ` },
276
+ // Empty-state headline + preview badge.
277
+ { anchor: ".pXSMma_headlineText", css: `
278
+ .pXSMma_headlineText{font-size:34px;line-height:42px;font-weight:700;letter-spacing:-.01em;background:linear-gradient(120deg,#152443 0%,var(--ds-brand-deep) 55%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
279
+ body[data-dshome-dark] .pXSMma_headlineText{background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
280
+ .pXSMma_previewBadge{border-radius:999px!important;background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent)!important;color:var(--ds-brand)!important}
281
+ .pXSMma_fish{color:var(--ds-brand)!important}
282
+ ` },
283
+ // Chat bubbles — liquid glass panes.
284
+ { anchor: ".gdEzaW_bubble", css: `
285
+ .gdEzaW_bubble{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.05);box-shadow:0 4px 16px rgba(13,30,60,.06)}
286
+ body[data-dshome-dark] .gdEzaW_bubble{background:rgba(21,42,86,.65)!important;border:1px solid rgba(255,255,255,.1);box-shadow:0 4px 16px rgba(0,0,0,.25)}
287
+ ` },
288
+ // Trajectory view bar.
289
+ { anchor: ".fV0t5q_root", css: `
290
+ .fV0t5q_root{background:rgba(255,255,255,.45)!important;border-bottom:1px solid rgba(0,0,0,.06)!important}
291
+ body[data-dshome-dark] .fV0t5q_root{background:rgba(13,30,60,.45)!important;border-bottom-color:rgba(255,255,255,.08)!important}
292
+ ` },
293
+ // Timeline root/plot/span.
294
+ { anchor: "._1p9O6q_root", css: `
295
+ ._1p9O6q_root{background:rgba(255,255,255,.3)!important;border-bottom:1px solid rgba(0,0,0,.05)!important}
296
+ ._1p9O6q_plot{background:transparent!important}
297
+ body[data-dshome-dark] ._1p9O6q_root{background:rgba(13,30,60,.3)!important}
298
+ ._1p9O6q_span[data-timeline-span=assistant]{background:var(--ds-brand)!important}
299
+ ._1p9O6q_span[data-timeline-span=user]{background:var(--ds-brand-light)!important}
300
+ ` },
301
+ // Progress panel entrance.
302
+ { anchor: ".Nqubda_panel", css: `
303
+ .Nqubda_panel{animation:dshome-pop-in .25s cubic-bezier(.2,.8,.2,1)}
304
+ ` },
305
+ // Settings overlay / modal — near-opaque glass with specular edge.
306
+ { anchor: ".VOzbGW_panel", css: `
307
+ .VOzbGW_mask{animation:dshome-fade-in .2s ease}
308
+ .VOzbGW_panel{background:rgba(255,255,255,.97)!important;animation:dshome-modal-in .4s cubic-bezier(.2,.8,.2,1)!important}
309
+ body[data-dshome-dark] .VOzbGW_panel{background:rgba(13,30,60,.96)!important}
310
+ .VOzbGW_overlay{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;inset:0!important}
311
+ ` }
312
+ ];
313
+
314
+ /** Append a style tag; returns a disposer that removes it. */
315
+ function injectStyle(css, label) {
316
+ const tag = document.createElement('style');
317
+ tag.dataset.plugin = PLUGIN_ID;
318
+ tag.dataset.pluginCss = PLUGIN_ID + '/' + label;
319
+ tag.textContent = css;
320
+ document.head.appendChild(tag);
321
+ return () => tag.remove();
322
+ }
323
+
324
+ /**
325
+ * Inject `css` only while `anchor` matches the DOM. If the anchor is
326
+ * absent at call time, wait up to 30s for it (covers shells that render
327
+ * after the plugin activates); if it never appears, skip the block and log
328
+ * one diagnostic line instead of shipping dead CSS against a renamed
329
+ * product surface. Returns a disposer for the whole install.
330
+ */
331
+ function installGuarded(anchor, css) {
332
+ let dispose = null;
333
+ const applyNow = () => {
334
+ if (dispose !== null) return true;
335
+ if (document.querySelector(anchor) === null) return false;
336
+ dispose = injectStyle(css, 'patch' + anchor);
337
+ return true;
338
+ };
339
+ if (applyNow()) {
340
+ return () => { if (dispose !== null) dispose(); };
341
+ }
342
+ let timer = 0;
343
+ let mo = new MutationObserver(() => {
344
+ if (!applyNow()) return;
345
+ mo.disconnect();
346
+ clearTimeout(timer);
347
+ });
348
+ try {
349
+ mo.observe(document.body, { childList: true, subtree: true });
350
+ } catch (error) {
351
+ mo = null;
352
+ }
353
+ timer = setTimeout(() => {
354
+ if (mo !== null) mo.disconnect();
355
+ if (dispose === null) {
356
+ console.info('[' + PLUGIN_ID + '] patch skipped: ' + anchor + ' not found in DOM (product surface renamed?)');
357
+ }
358
+ }, 30000);
359
+ return () => {
360
+ if (mo !== null) mo.disconnect();
361
+ clearTimeout(timer);
362
+ if (dispose !== null) dispose();
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Own the light/dark marker: reflect the theme service's active color
368
+ * scheme onto our own `data-dshome-dark` attribute, so dark styling never
369
+ * depends on the product's attribute name. Falls back to mirroring the
370
+ * product attribute only when the theme service or the `theme/change`
371
+ * subscription is unavailable.
372
+ */
373
+ function createDarkSync(ctx, theme) {
374
+ const apply = () => {
375
+ let dark = false;
376
+ if (theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
377
+ try {
378
+ const snap = theme.getTheme();
379
+ dark = snap !== undefined && snap !== null &&
380
+ snap.active !== undefined && snap.active !== null &&
381
+ snap.active.colorScheme === 'dark';
382
+ } catch (error) {
383
+ dark = false;
384
+ }
385
+ }
386
+ if (!dark) dark = document.body.hasAttribute('data-ds-dark-theme');
387
+ if (dark) document.body.setAttribute('data-dshome-dark', '');
388
+ else document.body.removeAttribute('data-dshome-dark');
389
+ };
390
+ apply();
391
+ let unsubscribe = null;
392
+ try {
393
+ unsubscribe = ctx.on('theme/change', apply);
394
+ } catch (error) {
395
+ unsubscribe = null;
396
+ }
397
+ let mirror = null;
398
+ if (unsubscribe === null || theme === undefined || theme === null || typeof theme.getTheme !== 'function') {
399
+ try {
400
+ mirror = new MutationObserver(apply);
401
+ mirror.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] });
402
+ } catch (error) {
403
+ mirror = null;
404
+ }
405
+ }
406
+ return () => {
407
+ if (unsubscribe !== null) unsubscribe();
408
+ if (mirror !== null) mirror.disconnect();
409
+ };
410
+ }
411
+
412
+ function startParticles() {
413
+ const canvas = document.createElement('canvas');
414
+ canvas.setAttribute('data-dsh-deepseek-canvas', '');
415
+ const style = canvas.style;
416
+ style.position = 'fixed';
417
+ style.top = '0';
418
+ style.left = '0';
419
+ style.width = '100%';
420
+ style.height = '100%';
421
+ style.zIndex = '-1';
422
+ style.pointerEvents = 'none';
423
+ style.opacity = '0.8';
424
+ document.body.appendChild(canvas);
425
+
426
+ const g = canvas.getContext('2d');
427
+ const isDark = () => document.body.hasAttribute('data-dshome-dark');
428
+ const activeColor = () => {
429
+ const c = document.body.getAttribute('data-dshome-color');
430
+ return c === 'red' || c === 'blue' ? c : 'green';
431
+ };
432
+ let running = true;
433
+ let rafId = 0;
434
+ let W = 0;
435
+ let H = 0;
436
+ let dpr = 1;
437
+ const COUNT = 72;
438
+ const parts = [];
439
+
440
+ function palette() {
441
+ const dark = isDark();
442
+ const color = activeColor();
443
+ if (color === 'red') {
444
+ return dark ? ['#ff4d4f', '#f5222d', '#ff7875', '#ffa39e', '#ffffff'] : ['#f5222d', '#ff7875', '#ff4d4f', '#ffffff'];
445
+ }
446
+ if (color === 'blue') {
447
+ return dark ? ['#5d79ff', '#4d6bfe', '#8fa8ff', '#ffffff', '#9db9ff'] : ['#4d6bfe', '#73a3d2', '#9db9ff', '#ffffff'];
448
+ }
449
+ return dark ? ['#34d399', '#059669', '#6ee7b7', '#a7f3d0', '#ffffff'] : ['#10b981', '#6ee7b7', '#34d399', '#ffffff'];
450
+ }
451
+
452
+ function linkBase() {
453
+ const dark = isDark();
454
+ const color = activeColor();
455
+ if (color === 'red') return dark ? 'rgba(255,77,79,' : 'rgba(245,34,45,';
456
+ if (color === 'blue') return dark ? 'rgba(93,121,255,' : 'rgba(77,107,254,';
457
+ return dark ? 'rgba(52,211,153,' : 'rgba(16,185,129,';
458
+ }
459
+
460
+ function resize() {
461
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
462
+ W = window.innerWidth;
463
+ H = window.innerHeight;
464
+ canvas.width = Math.round(W * dpr);
465
+ canvas.height = Math.round(H * dpr);
466
+ g.setTransform(dpr, 0, 0, dpr, 0, 0);
467
+ }
468
+
469
+ function make() {
470
+ const colors = palette();
471
+ return {
472
+ x: Math.random() * W,
473
+ y: Math.random() * H,
474
+ vx: (Math.random() - 0.5) * 0.28,
475
+ vy: (Math.random() - 0.5) * 0.28,
476
+ r: Math.random() * 1.7 + 0.6,
477
+ a: Math.random() * 0.55 + 0.15,
478
+ c: colors[(Math.random() * colors.length) | 0]
479
+ };
480
+ }
481
+
482
+ function step() {
483
+ if (!running) return;
484
+ g.clearRect(0, 0, W, H);
485
+ const base = linkBase();
486
+ for (let i = 0; i < parts.length; i++) {
487
+ for (let j = i + 1; j < parts.length; j++) {
488
+ const a = parts[i];
489
+ const b = parts[j];
490
+ const dx = a.x - b.x;
491
+ const dy = a.y - b.y;
492
+ const d2 = dx * dx + dy * dy;
493
+ if (d2 < 14400) {
494
+ const alpha = (1 - Math.sqrt(d2) / 120) * 0.16;
495
+ g.strokeStyle = base + alpha + ')';
496
+ g.lineWidth = 1;
497
+ g.beginPath();
498
+ g.moveTo(a.x, a.y);
499
+ g.lineTo(b.x, b.y);
500
+ g.stroke();
501
+ }
502
+ }
503
+ }
504
+ for (const p of parts) {
505
+ p.x += p.vx;
506
+ p.y += p.vy;
507
+ if (p.x < -20) p.x = W + 20;
508
+ else if (p.x > W + 20) p.x = -20;
509
+ if (p.y < -20) p.y = H + 20;
510
+ else if (p.y > H + 20) p.y = -20;
511
+ g.globalAlpha = p.a;
512
+ g.fillStyle = p.c;
513
+ g.beginPath();
514
+ g.arc(p.x, p.y, p.r, 0, Math.PI * 2);
515
+ g.fill();
516
+ }
517
+ g.globalAlpha = 1;
518
+ rafId = requestAnimationFrame(step);
519
+ }
520
+
521
+ const refreshColors = () => {
522
+ const colors = palette();
523
+ for (const p of parts) p.c = colors[(Math.random() * colors.length) | 0];
524
+ };
525
+ const unsubscribeColor = subscribeColor(refreshColors);
526
+
527
+ window.addEventListener('resize', resize);
528
+ resize();
529
+ for (let i = 0; i < COUNT; i++) parts.push(make());
530
+ rafId = requestAnimationFrame(step);
531
+
532
+ return () => {
533
+ running = false;
534
+ cancelAnimationFrame(rafId);
535
+ window.removeEventListener('resize', resize);
536
+ unsubscribeColor();
537
+ canvas.remove();
538
+ };
539
+ }
540
+
541
+ function watchSettingsSection() {
542
+ let lastChild = null;
543
+ const mo = new MutationObserver(() => {
544
+ const options = document.querySelector('.VOzbGW_options');
545
+ if (options === null) return;
546
+ const child = options.firstElementChild;
547
+ if (child !== lastChild) {
548
+ lastChild = child;
549
+ if (child !== null) {
550
+ child.style.animation = 'none';
551
+ void child.offsetWidth;
552
+ child.style.animation = 'dshome-fade-in .25s ease';
553
+ }
554
+ }
555
+ });
556
+ mo.observe(document.body, { subtree: true, childList: true });
557
+ return () => mo.disconnect();
558
+ }
559
+
560
+ function watchBrandLink() {
561
+ const onClick = (event) => {
562
+ const brand = event.target && event.target.closest ? event.target.closest('.hHd-Xa_brand') : null;
563
+ if (brand === null) return;
564
+ event.preventDefault();
565
+ event.stopPropagation();
566
+ window.open('https://www.deepseek.com/', '_blank', 'noopener,noreferrer');
567
+ };
568
+ document.addEventListener('click', onClick, true);
569
+ return () => document.removeEventListener('click', onClick, true);
570
+ }
571
+
572
+ function watchHeaderInteraction() {
573
+ const EASE = 'cubic-bezier(.2,.8,.2,1)';
574
+ const DUR = '.3s';
575
+ const timers = [];
576
+
577
+ const headerOf = (node) => (node && node.closest ? node.closest('.wSkVaW_header') : null);
578
+ const isTrajectory = (header) => {
579
+ const root = header.closest('.wSkVaW_root');
580
+ return root !== null && root.querySelector('.fV0t5q_root') !== null;
581
+ };
582
+
583
+ const enter = (header) => {
584
+ const actions = header.querySelector('.wSkVaW_headerActions');
585
+ const crumbs = header.querySelector('.wSkVaW_crumbs');
586
+ if (crumbs === null) return;
587
+
588
+ const actionsFirst = actions ? actions.getBoundingClientRect() : null;
589
+ const crumbsFirst = crumbs.getBoundingClientRect();
590
+
591
+ header.classList.add('dshome-swap');
592
+
593
+ const actionsFlow = actions ? actions.getBoundingClientRect() : null;
594
+ const crumbsFlow = crumbs.getBoundingClientRect();
595
+
596
+ if (actions && actionsFirst && actionsFlow) {
597
+ const dx = actionsFirst.left - actionsFlow.left;
598
+ const dy = actionsFirst.top - actionsFlow.top;
599
+ actions.style.transition = 'none';
600
+ actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
601
+ }
602
+
603
+ const cdx = crumbsFirst.left - crumbsFlow.left;
604
+ const cdy = crumbsFirst.top - crumbsFlow.top;
605
+ crumbs.style.transition = 'none';
606
+ crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
607
+
608
+ void header.offsetWidth;
609
+
610
+ if (actions) {
611
+ actions.style.transition = 'transform ' + DUR + ' ' + EASE;
612
+ actions.style.transform = 'translate(0,0)';
613
+ }
614
+ crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
615
+ crumbs.style.transform = 'translate(0,0)';
616
+
617
+ const timer = setTimeout(() => {
618
+ if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
619
+ crumbs.style.transition = '';
620
+ crumbs.style.transform = '';
621
+ }, 320);
622
+ timers.push(timer);
623
+ };
624
+
625
+ const leave = (header) => {
626
+ const actions = header.querySelector('.wSkVaW_headerActions');
627
+ const crumbs = header.querySelector('.wSkVaW_crumbs');
628
+
629
+ const actionsHover = actions ? actions.getBoundingClientRect() : null;
630
+ const crumbsHover = crumbs ? crumbs.getBoundingClientRect() : null;
631
+
632
+ header.classList.remove('dshome-swap');
633
+
634
+ if (actions && actionsHover) {
635
+ const n = actions.getBoundingClientRect();
636
+ const dx = actionsHover.left - n.left;
637
+ const dy = actionsHover.top - n.top;
638
+ actions.style.transition = 'none';
639
+ actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
640
+ }
641
+
642
+ if (crumbs && crumbsHover) {
643
+ const n = crumbs.getBoundingClientRect();
644
+ const cdx = crumbsHover.left - n.left;
645
+ const cdy = crumbsHover.top - n.top;
646
+ crumbs.style.transition = 'none';
647
+ crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
648
+ }
649
+
650
+ void header.offsetWidth;
651
+
652
+ if (actions) {
653
+ actions.style.transition = 'transform ' + DUR + ' ' + EASE;
654
+ actions.style.transform = 'translate(0,0)';
655
+ }
656
+ if (crumbs) {
657
+ crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
658
+ crumbs.style.transform = 'translate(0,0)';
659
+ }
660
+
661
+ const timer = setTimeout(() => {
662
+ if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
663
+ if (crumbs) { crumbs.style.transition = ''; crumbs.style.transform = ''; }
664
+ }, 320);
665
+ timers.push(timer);
666
+ };
667
+
668
+ const over = (event) => {
669
+ const header = headerOf(event.target);
670
+ if (header === null || isTrajectory(header)) return;
671
+ if (header.classList.contains('dshome-swap')) return;
672
+ const from = event.relatedTarget;
673
+ if (from && header.contains(from)) return;
674
+ enter(header);
675
+ };
676
+
677
+ const out = (event) => {
678
+ const header = headerOf(event.target);
679
+ if (header === null) return;
680
+ if (!header.classList.contains('dshome-swap')) return;
681
+ const to = event.relatedTarget;
682
+ if (to && header.contains(to)) return;
683
+ leave(header);
684
+ };
685
+
686
+ document.addEventListener('mouseover', over);
687
+ document.addEventListener('mouseout', out);
688
+
689
+ return () => {
690
+ document.removeEventListener('mouseover', over);
691
+ document.removeEventListener('mouseout', out);
692
+ timers.forEach((t) => clearTimeout(t));
693
+ document.querySelectorAll('.wSkVaW_header.dshome-swap').forEach((h) => {
694
+ h.classList.remove('dshome-swap');
695
+ const a = h.querySelector('.wSkVaW_headerActions');
696
+ const c = h.querySelector('.wSkVaW_crumbs');
697
+ if (a) { a.style.transition = ''; a.style.transform = ''; }
698
+ if (c) { c.style.transition = ''; c.style.transform = ''; }
699
+ });
700
+ };
701
+ }
702
+
703
+ /**
704
+ * Add an "Open workspace" entry to the folder row's (workspace row) three-dot
705
+ * menu. The workspace browser renders its row menus internally with no plugin
706
+ * slot, so the theme patches the popup DOM: it tracks the ellipsis anchor of a
707
+ * project row, and when the portaled [role=menu] appears it inserts an item
708
+ * that resolves the row's workspace (title match against the workspaces
709
+ * store, DOM-order fallback) and asks the host half (connection RPC) to open
710
+ * the workspace's directory with the OS file manager in the foreground.
711
+ * The private host channel is the primary reveal path; `workspaces.openPath`
712
+ * is used only when it still exists (dsh ≤0.1.1), and a failed reveal is
713
+ * reported on the console instead of failing silently.
714
+ */
715
+ function watchWorkspaceMenu(ctx) {
716
+ let workspaces;
717
+ try {
718
+ workspaces = ctx.get('workspaces');
719
+ } catch (error) {
720
+ workspaces = undefined;
721
+ }
722
+ let connection;
723
+ try {
724
+ connection = ctx.get('connection');
725
+ } catch (error) {
726
+ connection = undefined;
727
+ }
728
+ if (workspaces === undefined || workspaces === null || workspaces.list === undefined) return () => {};
729
+
730
+ const ROW = '.YDXeBa_projectRow';
731
+ const ACTIONS = '.YDXeBa_rowActions';
732
+ const LABEL = '.YDXeBa_title';
733
+ const CLS = 'dshome-ws-open';
734
+ const FOLDER_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
735
+
736
+ let label = '打开工作区';
737
+ try {
738
+ const locale = ctx.get('locale');
739
+ if (locale !== undefined) {
740
+ const active = locale.getLocale().active;
741
+ if (active !== 'zh' && active !== 'zh-CN' && active !== 'zh-Hans') label = 'Open workspace';
742
+ }
743
+ } catch (error) {}
744
+
745
+ let anchorRow = null;
746
+
747
+ const resolveWorkspace = (row) => {
748
+ try {
749
+ const items = workspaces.list.getSnapshot().items;
750
+ if (!Array.isArray(items)) return undefined;
751
+ const titleEl = row.querySelector(LABEL);
752
+ const labelText = titleEl === null ? '' : (titleEl.textContent || '').trim();
753
+ if (labelText === '') return undefined;
754
+ const byTitle = items.filter((w) => w !== null && w !== undefined && w.title === labelText);
755
+ if (byTitle.length === 1) return byTitle[0];
756
+ const rows = Array.from(document.querySelectorAll(ROW));
757
+ const index = rows.indexOf(row);
758
+ if (index === -1) return undefined;
759
+ let ordinal = 0;
760
+ for (let i = 0; i < index; i++) {
761
+ const t = rows[i].querySelector(LABEL);
762
+ const l = t === null ? '' : (t.textContent || '').trim();
763
+ if (l !== '' && l !== 'Ungrouped') ordinal += 1;
764
+ }
765
+ const candidate = items[ordinal];
766
+ return candidate === undefined || candidate === null ? undefined : candidate;
767
+ } catch (error) {
768
+ return undefined;
769
+ }
770
+ };
771
+
772
+ const track = (target) => {
773
+ anchorRow = null;
774
+ if (target === null || typeof target.closest !== 'function') return;
775
+ const button = target.closest('button');
776
+ if (button === null) return;
777
+ const actions = button.closest(ACTIONS);
778
+ if (actions === null) return;
779
+ const row = actions.closest(ROW);
780
+ if (row === null) return;
781
+ if (button !== actions.querySelector('button')) return;
782
+ anchorRow = row;
783
+ };
784
+
785
+ const onClickCapture = (event) => track(event.target);
786
+ const onFocusIn = (event) => track(event.target);
787
+ document.addEventListener('click', onClickCapture, true);
788
+ document.addEventListener('focusin', onFocusIn, true);
789
+
790
+ const injectItem = (menu) => {
791
+ if (menu === null || menu.querySelector('.' + CLS) !== null) return;
792
+ const viewport = menu.querySelector('[role="presentation"]');
793
+ if (viewport === null) return;
794
+ const row = anchorRow;
795
+ if (row === null || !document.contains(row)) return;
796
+ const workspace = resolveWorkspace(row);
797
+ if (workspace === undefined) return;
798
+
799
+ const wrap = document.createElement('div');
800
+ wrap.className = CLS + '-wrap';
801
+ const item = document.createElement('button');
802
+ item.type = 'button';
803
+ item.className = CLS;
804
+ item.setAttribute('role', 'menuitem');
805
+ item.title = label;
806
+ item.setAttribute('aria-label', label);
807
+ const icon = document.createElement('span');
808
+ icon.className = CLS + '-icon';
809
+ icon.innerHTML = FOLDER_SVG;
810
+ const text = document.createElement('span');
811
+ text.className = CLS + '-label';
812
+ text.textContent = label;
813
+ item.appendChild(icon);
814
+ item.appendChild(text);
815
+ item.addEventListener('click', (event) => {
816
+ event.preventDefault();
817
+ event.stopPropagation();
818
+ const path = workspace.path;
819
+ // The host bridge is a same-origin webServer route (see bridgeCall).
820
+ const fallback = (reason) => {
821
+ if (typeof workspaces.openPath === 'function') {
822
+ try {
823
+ workspaces.openPath(path);
824
+ return;
825
+ } catch (error) {
826
+ console.warn('[deepseek-style-theme] workspaces.openPath failed:', error);
827
+ }
828
+ }
829
+ console.warn(
830
+ '[deepseek-style-theme] 打开工作区失败(' + reason + '):' +
831
+ '私有 RPC 通道不可用,且当前 dsh 版本已移除 workspaces.openPath。' +
832
+ '请确认插件为 1.38.4+ 并重启 dsh web(旧版宿主端因缺少 webServer 注入无法注册通道)。'
833
+ );
834
+ };
835
+ Promise.resolve(bridgeCall('dshome/explorer.open', { path: path }))
836
+ .then((result) => {
837
+ if (result !== undefined && result !== null && result.ok === true) return;
838
+ const detail = result !== undefined && result !== null && result.error !== undefined
839
+ ? String(result.error.message)
840
+ : 'no result';
841
+ fallback('bridge returned ' + detail);
842
+ })
843
+ .catch((error) => {
844
+ fallback('bridge call threw: ' + (error instanceof Error ? error.message : String(error)));
845
+ });
846
+ try {
847
+ document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
848
+ } catch (error) {}
849
+ });
850
+ wrap.appendChild(item);
851
+ viewport.insertBefore(wrap, viewport.firstChild);
852
+ };
853
+
854
+ const observer = new MutationObserver((records) => {
855
+ for (const record of records) {
856
+ for (const node of record.addedNodes) {
857
+ if (!(node instanceof Element)) continue;
858
+ if (node.matches('[role="menu"]')) {
859
+ injectItem(node);
860
+ continue;
861
+ }
862
+ node.querySelectorAll('[role="menu"]').forEach(injectItem);
863
+ }
864
+ }
865
+ });
866
+ observer.observe(document.body, { childList: true, subtree: true });
867
+
868
+ return () => {
869
+ observer.disconnect();
870
+ document.removeEventListener('click', onClickCapture, true);
871
+ document.removeEventListener('focusin', onFocusIn, true);
872
+ };
873
+ }
874
+
875
+ /**
876
+ * Action menu for the product's delivered/changed files
877
+ * (`@deepseek-ai/dsh-client-ui-deliverables`), which renders two separate
878
+ * surfaces:
879
+ *
880
+ * - `[data-produced-files-row]` — the "本轮文件改动" chips, one
881
+ * `button[title=absolutePath]` per changed path, whose only own action
882
+ * is the product's preview opener;
883
+ * - `[data-presented-file]` — the declared-file cards, whose overlay button
884
+ * previews and whose chevron already offers default-app open and reveal.
885
+ *
886
+ * Both now open this menu: the changed-file chips on a plain left click
887
+ * (clicking one previously looked like nothing happened) and both surfaces
888
+ * on right click. The product's own action is preserved as a menu item, so
889
+ * hijacking the chip click trades nothing away. "Copy path" does not exist
890
+ * anywhere in the product — the absolute path is otherwise only reachable by
891
+ * reading a `title` tooltip by hand — so it is also added to the product's
892
+ * own chevron menu.
893
+ *
894
+ * Both anchors are stable product attributes rather than build-hashed class
895
+ * names, and each surface already carries the absolute path in its `title`
896
+ * (a card via the product's own `resolveWorkspacePath`), so this patch never
897
+ * re-derives a workspace.
898
+ */
899
+ function watchFileCardMenu(ctx) {
900
+ const CARD = '[data-presented-file]';
901
+ // The product renders changed files through a *different* component:
902
+ // ProducedFiles draws one `button[title=absolutePath]` per path inside
903
+ // `[data-produced-files-row]`. Those chips are not
904
+ // `[data-presented-file]` cards at all, so they need their own anchor.
905
+ const PRODUCED = '[data-produced-files-row] button[title]';
906
+ const MENU = '[role="menu"]';
907
+ const CLS = 'dshome-file-menu';
908
+ const ITEM_CLS = CLS + '-item';
909
+ const TOAST_CLS = CLS + '-toast';
910
+ const COPY_CLS = 'dshome-file-copy';
911
+ const ICON_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>';
912
+ const ICON_REVEAL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
913
+ const ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
914
+ const ICON_PREVIEW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>';
915
+ const CSS = `
916
+ .${CLS}{position:fixed;z-index:2147483000;min-width:198px;padding:6px;border-radius:14px;background:rgba(255,255,255,.94);border:.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));box-shadow:0 18px 44px rgba(15,30,60,.2);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);animation:dshome-menu-pop .16s cubic-bezier(.2,.8,.2,1);transform-origin:top left}
917
+ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 44px rgba(0,0,0,.5)}
918
+ .${ITEM_CLS},.${COPY_CLS}{display:flex;align-items:center;gap:9px;box-sizing:border-box;width:100%;padding:8px 10px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#152443);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;transition:background-color .12s ease,transform .12s ease}
919
+ .${ITEM_CLS}:hover,.${COPY_CLS}:hover{background:color-mix(in srgb,var(--ds-brand,#4d6bfe) 13%,transparent);transform:translateX(1px)}
920
+ .${ITEM_CLS}:focus-visible,.${COPY_CLS}:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}
921
+ .${ITEM_CLS} svg,.${COPY_CLS} svg{flex:none;width:15px;height:15px;opacity:.72}
922
+ .${ITEM_CLS}>span,.${COPY_CLS}>span{display:inline-flex;align-items:center;min-width:0}
923
+ .${CLS}-sep{height:1px;margin:5px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
924
+ .${TOAST_CLS}{position:fixed;z-index:2147483001;left:50%;bottom:56px;transform:translateX(-50%);padding:8px 15px;border-radius:999px;background:rgba(20,36,70,.93);color:#fff;font-size:12px;line-height:18px;pointer-events:none;animation:dshome-fade-in .18s ease}
925
+ @keyframes dshome-menu-pop{from{opacity:0;transform:scale(.96) translateY(-4px)}to{opacity:1;transform:none}}
926
+ `;
927
+
928
+ const isZh = () => {
929
+ try {
930
+ const locale = ctx.get('locale');
931
+ if (locale === undefined || locale === null) return true;
932
+ const active = locale.getLocale().active;
933
+ return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
934
+ } catch (error) {
935
+ return true;
936
+ }
937
+ };
938
+ const labels = () => (isZh() ? {
939
+ open: '用默认应用打开',
940
+ reveal: '打开所在文件夹',
941
+ copy: '复制路径',
942
+ preview: '在侧边栏预览',
943
+ previewProduced: '打开预览',
944
+ copied: '已复制路径',
945
+ copyDenied: '浏览器拒绝了剪贴板访问,请手动复制',
946
+ failed: '操作失败(宿主端未响应)'
947
+ } : {
948
+ open: 'Open with default app',
949
+ reveal: 'Open containing folder',
950
+ copy: 'Copy path',
951
+ preview: 'Preview in sidebar',
952
+ previewProduced: 'Open preview',
953
+ copied: 'Path copied',
954
+ copyDenied: 'Clipboard access was denied — copy it manually',
955
+ failed: 'Action failed (no response from the host)'
956
+ });
957
+
958
+ /** The absolute path the product already resolved onto a card. */
959
+ const pathOf = (card) => {
960
+ const overlay = card.querySelector('button[title]');
961
+ if (overlay === null) return '';
962
+ const value = overlay.getAttribute('title');
963
+ return typeof value === 'string' ? value.trim() : '';
964
+ };
965
+
966
+ /**
967
+ * Resolve a click target to the file it represents. A changed-file chip
968
+ * is both the anchor and the button whose own click opens the product's
969
+ * preview; a presented card carries that button as an overlay. Returns
970
+ * null when the target is neither.
971
+ */
972
+ const surfaceOf = (target) => {
973
+ if (target === null || typeof target.closest !== 'function') return null;
974
+ const chip = target.closest(PRODUCED);
975
+ if (chip !== null) {
976
+ const chipPath = chip.getAttribute('title');
977
+ const path = typeof chipPath === 'string' ? chipPath.trim() : '';
978
+ return path === '' ? null : { path: path, button: chip, produced: true };
979
+ }
980
+ const card = target.closest(CARD);
981
+ if (card === null) return null;
982
+ const overlay = card.querySelector('button[title]');
983
+ if (overlay === null) return null;
984
+ const overlayPath = overlay.getAttribute('title');
985
+ const path = typeof overlayPath === 'string' ? overlayPath.trim() : '';
986
+ return path === '' ? null : { path: path, button: overlay, produced: false };
987
+ };
988
+
989
+ /**
990
+ * Run the product's own action for a surface by re-dispatching its click.
991
+ * The flag lets that synthetic click past our capture handler, which
992
+ * would otherwise open this menu again instead of the product's preview.
993
+ */
994
+ let replaying = false;
995
+ const preview = (surface) => {
996
+ replaying = true;
997
+ try {
998
+ surface.button.click();
999
+ } finally {
1000
+ replaying = false;
1001
+ }
1002
+ };
1003
+
1004
+ let menu = null;
1005
+ let toastNode = null;
1006
+ const closeMenu = () => {
1007
+ if (menu === null) return;
1008
+ menu.remove();
1009
+ menu = null;
1010
+ };
1011
+ const toast = (text) => {
1012
+ if (toastNode !== null) toastNode.remove();
1013
+ const node = document.createElement('div');
1014
+ node.className = TOAST_CLS;
1015
+ node.setAttribute('role', 'status');
1016
+ node.textContent = text;
1017
+ document.body.appendChild(node);
1018
+ toastNode = node;
1019
+ setTimeout(() => {
1020
+ node.remove();
1021
+ if (toastNode === node) toastNode = null;
1022
+ }, 1800);
1023
+ };
1024
+ // localhost is a secure context, so the async clipboard exists in the
1025
+ // real GUI; a rejection is still reported rather than swallowed.
1026
+ const copyPath = (path) => {
1027
+ const clipboard = navigator.clipboard;
1028
+ if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') {
1029
+ return Promise.resolve(false);
1030
+ }
1031
+ return Promise.resolve(clipboard.writeText(path)).then(() => true).catch(() => false);
1032
+ };
1033
+ const invoke = (endpoint, path) => Promise.resolve(bridgeCall(endpoint, { path: path }))
1034
+ .then((result) => result !== undefined && result !== null && result.ok === true)
1035
+ .catch(() => false);
1036
+
1037
+ const buildItem = (icon, label, onPick) => {
1038
+ const item = document.createElement('button');
1039
+ item.type = 'button';
1040
+ item.className = ITEM_CLS;
1041
+ item.setAttribute('role', 'menuitem');
1042
+ item.title = label;
1043
+ item.setAttribute('aria-label', label);
1044
+ const glyph = document.createElement('span');
1045
+ glyph.innerHTML = icon;
1046
+ const caption = document.createElement('span');
1047
+ caption.textContent = label;
1048
+ item.appendChild(glyph);
1049
+ item.appendChild(caption);
1050
+ item.addEventListener('click', (event) => {
1051
+ event.preventDefault();
1052
+ event.stopPropagation();
1053
+ closeMenu();
1054
+ onPick();
1055
+ });
1056
+ return item;
1057
+ };
1058
+
1059
+ const openMenu = (surface, clientX, clientY) => {
1060
+ closeMenu();
1061
+ const path = surface.path;
1062
+ const text = labels();
1063
+ const node = document.createElement('div');
1064
+ node.className = CLS;
1065
+ node.setAttribute('role', 'menu');
1066
+ node.appendChild(buildItem(ICON_OPEN, text.open, () => {
1067
+ invoke('dshome/file.open', path).then((ok) => { if (!ok) toast(text.failed); });
1068
+ }));
1069
+ node.appendChild(buildItem(ICON_REVEAL, text.reveal, () => {
1070
+ invoke('dshome/file.reveal', path).then((ok) => { if (!ok) toast(text.failed); });
1071
+ }));
1072
+ node.appendChild(buildItem(ICON_COPY, text.copy, () => {
1073
+ copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
1074
+ }));
1075
+ const separator = document.createElement('div');
1076
+ separator.className = CLS + '-sep';
1077
+ node.appendChild(separator);
1078
+ node.appendChild(buildItem(ICON_PREVIEW, surface.produced ? text.previewProduced : text.preview, () => {
1079
+ preview(surface);
1080
+ }));
1081
+ document.body.appendChild(node);
1082
+ // Clamp to the viewport so a card near an edge never opens a menu
1083
+ // the user cannot reach.
1084
+ const box = node.getBoundingClientRect();
1085
+ node.style.left = Math.max(8, Math.min(clientX, window.innerWidth - box.width - 8)) + 'px';
1086
+ node.style.top = Math.max(8, Math.min(clientY, window.innerHeight - box.height - 8)) + 'px';
1087
+ menu = node;
1088
+ const first = node.querySelector('.' + ITEM_CLS);
1089
+ if (first !== null) first.focus();
1090
+ };
1091
+
1092
+ const onContextMenu = (event) => {
1093
+ const surface = surfaceOf(event.target);
1094
+ if (surface === null) return;
1095
+ event.preventDefault();
1096
+ event.stopPropagation();
1097
+ openMenu(surface, event.clientX, event.clientY);
1098
+ };
1099
+ // A changed-file chip's only own action is the product's preview, and
1100
+ // in practice clicking one looks like nothing happens. Those chips
1101
+ // therefore open this menu on a plain left click, with the product's
1102
+ // own action kept as a menu item. Presented cards keep their left
1103
+ // click — their overlay visibly previews — and use the right click.
1104
+ const onClickCapture = (event) => {
1105
+ if (replaying || event.defaultPrevented) return;
1106
+ if (event.button !== undefined && event.button !== 0) return;
1107
+ const surface = surfaceOf(event.target);
1108
+ if (surface === null || !surface.produced) return;
1109
+ event.preventDefault();
1110
+ event.stopPropagation();
1111
+ openMenu(surface, event.clientX, event.clientY);
1112
+ };
1113
+ const onPointerDown = (event) => {
1114
+ if (menu === null) return;
1115
+ if (event.target instanceof Node && menu.contains(event.target)) return;
1116
+ closeMenu();
1117
+ };
1118
+ const onKeyDown = (event) => {
1119
+ if (event.key === 'Escape') closeMenu();
1120
+ };
1121
+ document.addEventListener('contextmenu', onContextMenu, true);
1122
+ document.addEventListener('click', onClickCapture, true);
1123
+ document.addEventListener('pointerdown', onPointerDown, true);
1124
+ document.addEventListener('keydown', onKeyDown, true);
1125
+ window.addEventListener('resize', closeMenu, true);
1126
+ window.addEventListener('scroll', closeMenu, true);
1127
+
1128
+ // "Copy path" is the one gesture the product lacks, so it is also added
1129
+ // to the product's own chevron menu, where file actions are looked for.
1130
+ let anchorCard = null;
1131
+ const track = (target) => {
1132
+ anchorCard = null;
1133
+ if (target === null || typeof target.closest !== 'function') return;
1134
+ const button = target.closest('button');
1135
+ if (button === null || button.getAttribute('aria-haspopup') !== 'menu') return;
1136
+ const card = button.closest(CARD);
1137
+ if (card === null) return;
1138
+ anchorCard = card;
1139
+ };
1140
+ const onTrackCapture = (event) => track(event.target);
1141
+ document.addEventListener('click', onTrackCapture, true);
1142
+ document.addEventListener('focusin', onTrackCapture, true);
1143
+
1144
+ const injectCopyItem = (node) => {
1145
+ if (anchorCard === null || !document.contains(anchorCard)) return;
1146
+ if (node.querySelector('.' + COPY_CLS) !== null) return;
1147
+ const card = anchorCard;
1148
+ const path = pathOf(card);
1149
+ if (path === '') return;
1150
+ const viewport = node.querySelector('[role="presentation"]');
1151
+ if (viewport === null) return;
1152
+ const text = labels();
1153
+ const item = document.createElement('button');
1154
+ item.type = 'button';
1155
+ item.className = COPY_CLS;
1156
+ item.setAttribute('role', 'menuitem');
1157
+ item.title = text.copy;
1158
+ item.setAttribute('aria-label', text.copy);
1159
+ const glyph = document.createElement('span');
1160
+ glyph.innerHTML = ICON_COPY;
1161
+ const caption = document.createElement('span');
1162
+ caption.textContent = text.copy;
1163
+ item.appendChild(glyph);
1164
+ item.appendChild(caption);
1165
+ item.addEventListener('click', (event) => {
1166
+ event.preventDefault();
1167
+ event.stopPropagation();
1168
+ copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
1169
+ try {
1170
+ document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
1171
+ } catch (error) {}
1172
+ });
1173
+ viewport.appendChild(item);
1174
+ };
1175
+
1176
+ const observer = new MutationObserver((records) => {
1177
+ for (const record of records) {
1178
+ for (const added of record.addedNodes) {
1179
+ if (!(added instanceof Element)) continue;
1180
+ if (added.matches(MENU)) {
1181
+ injectCopyItem(added);
1182
+ continue;
1183
+ }
1184
+ added.querySelectorAll(MENU).forEach(injectCopyItem);
1185
+ }
1186
+ }
1187
+ });
1188
+ observer.observe(document.body, { childList: true, subtree: true });
1189
+
1190
+ const disposeStyle = injectStyle(CSS, 'file-card-menu.css');
1191
+ return () => {
1192
+ disposeStyle();
1193
+ observer.disconnect();
1194
+ closeMenu();
1195
+ if (toastNode !== null) toastNode.remove();
1196
+ document.removeEventListener('contextmenu', onContextMenu, true);
1197
+ document.removeEventListener('click', onClickCapture, true);
1198
+ document.removeEventListener('pointerdown', onPointerDown, true);
1199
+ document.removeEventListener('keydown', onKeyDown, true);
1200
+ document.removeEventListener('click', onTrackCapture, true);
1201
+ document.removeEventListener('focusin', onTrackCapture, true);
1202
+ window.removeEventListener('resize', closeMenu, true);
1203
+ window.removeEventListener('scroll', closeMenu, true);
1204
+ };
1205
+ }
1206
+
1207
+ /**
1208
+ * "Running subagents" floating panel. While any subagent-origin session in
1209
+ * the sessions list mirror reports `running`, the panel shows every such
1210
+ * subagent with live progress: elapsed active time folded from the
1211
+ * `subagentTiming` projection (settled + open-turn span), token usage
1212
+ * folded from the `tokenUsage` projection, and an indeterminate shimmer
1213
+ * bar. Clicking a row opens that subagent session (catalog address when
1214
+ * retained, plain open otherwise). Reads only leaf fields of the live
1215
+ * list snapshot; every side effect (DOM, subscription, timers) is owned
1216
+ * by the returned disposer.
1217
+ */
1218
+ function watchRunningSubagents(ctx) {
1219
+ let sessions;
1220
+ try {
1221
+ sessions = ctx.get('sessions');
1222
+ } catch (error) {
1223
+ sessions = undefined;
1224
+ }
1225
+ if (
1226
+ sessions === undefined || sessions === null ||
1227
+ sessions.list === undefined || sessions.list === null ||
1228
+ typeof sessions.list.getSnapshot !== 'function' ||
1229
+ typeof sessions.list.subscribe !== 'function'
1230
+ ) {
1231
+ return () => {};
1232
+ }
1233
+
1234
+ let locale;
1235
+ try {
1236
+ locale = ctx.get('locale');
1237
+ } catch (error) {
1238
+ locale = undefined;
1239
+ }
1240
+ let connection;
1241
+ try {
1242
+ connection = ctx.get('connection');
1243
+ } catch (error) {
1244
+ connection = undefined;
1245
+ }
1246
+ const isZh = () => {
1247
+ try {
1248
+ if (locale === undefined || locale === null || locale.getLocale === undefined) return true;
1249
+ const active = locale.getLocale().active;
1250
+ return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
1251
+ } catch (error) {
1252
+ return true;
1253
+ }
1254
+ };
1255
+
1256
+ const tokenTotal = (usage) => {
1257
+ if (usage === undefined || usage === null) return undefined;
1258
+ return (
1259
+ (Number(usage.uncachedInputTokens) || 0) +
1260
+ (Number(usage.outputTokens) || 0) +
1261
+ (Number(usage.cacheReadTokens) || 0) +
1262
+ (Number(usage.cacheWriteTokens) || 0)
1263
+ );
1264
+ };
1265
+ const formatTokens = (value) => {
1266
+ const scaled = (next) => (next >= 100 ? String(Math.round(next)) : String(Math.round(next * 10) / 10));
1267
+ if (value < 1000) return String(value);
1268
+ if (value < 1e6) return scaled(value / 1e3) + 'K';
1269
+ return scaled(value / 1e6) + 'M';
1270
+ };
1271
+ const formatDuration = (ms) => {
1272
+ const total = Math.max(0, Math.floor(ms / 1000));
1273
+ const seconds = total % 60;
1274
+ const minutes = Math.floor(total / 60) % 60;
1275
+ const hours = Math.floor(total / 3600);
1276
+ if (isZh()) {
1277
+ if (hours > 0) return hours + '小时' + String(minutes).padStart(2, '0') + '分';
1278
+ if (minutes > 0) return minutes + '分' + String(seconds).padStart(2, '0') + '秒';
1279
+ return seconds + '秒';
1280
+ }
1281
+ if (hours > 0) return hours + 'h ' + String(minutes).padStart(2, '0') + 'm';
1282
+ if (minutes > 0) return minutes + 'm ' + String(seconds).padStart(2, '0') + 's';
1283
+ return seconds + 's';
1284
+ };
1285
+ const runningTitle = (count) => (
1286
+ isZh()
1287
+ ? count + ' 个子代理正在运行'
1288
+ : (count === 1 ? '1 subagent running' : count + ' subagents running')
1289
+ );
1290
+
1291
+ const styleTag = document.createElement('style');
1292
+ styleTag.dataset.plugin = PLUGIN_ID;
1293
+ styleTag.dataset.pluginCss = PLUGIN_ID + '/subagent-panel.css';
1294
+ styleTag.textContent = SUBAGENT_PANEL_CSS;
1295
+ document.head.appendChild(styleTag);
1296
+
1297
+ const CHEVRON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3.5 5 6.5 8 3.5"/></svg>';
1298
+
1299
+ const panel = document.createElement('div');
1300
+ panel.className = 'dshome-sa';
1301
+ panel.hidden = true;
1302
+ panel.setAttribute('role', 'status');
1303
+ panel.setAttribute('aria-live', 'polite');
1304
+
1305
+ const head = document.createElement('div');
1306
+ head.className = 'dshome-sa-head';
1307
+ const pulse = document.createElement('span');
1308
+ pulse.className = 'dshome-sa-pulse';
1309
+ const titleEl = document.createElement('span');
1310
+ titleEl.className = 'dshome-sa-title';
1311
+ const collapseBtn = document.createElement('button');
1312
+ collapseBtn.type = 'button';
1313
+ collapseBtn.className = 'dshome-sa-collapse';
1314
+ collapseBtn.innerHTML = CHEVRON_SVG;
1315
+ head.appendChild(pulse);
1316
+ head.appendChild(titleEl);
1317
+ head.appendChild(collapseBtn);
1318
+
1319
+ const listEl = document.createElement('div');
1320
+ listEl.className = 'dshome-sa-list';
1321
+ panel.appendChild(head);
1322
+ panel.appendChild(listEl);
1323
+ document.body.appendChild(panel);
1324
+
1325
+ const timingOf = (s) => (
1326
+ s.projectionValues === undefined || s.projectionValues === null
1327
+ ? undefined
1328
+ : s.projectionValues.subagentTiming
1329
+ );
1330
+ const startOf = (s) => {
1331
+ const t = timingOf(s);
1332
+ const since = t === undefined || t === null || t.active === undefined || t.active === null ? undefined : t.active.since;
1333
+ if (typeof since === 'number') return since;
1334
+ return typeof s.updatedAt === 'number' ? s.updatedAt : 0;
1335
+ };
1336
+ const elapsedMs = (s) => {
1337
+ const t = timingOf(s);
1338
+ if (t === undefined || t === null) return undefined;
1339
+ const settled = typeof t.settledMs === 'number' ? t.settledMs : 0;
1340
+ const since = t.active === undefined || t.active === null ? undefined : t.active.since;
1341
+ if (typeof since !== 'number') return settled;
1342
+ return settled + Math.max(0, Date.now() - since);
1343
+ };
1344
+ const metaText = (s) => {
1345
+ const parts = [];
1346
+ const elapsed = elapsedMs(s);
1347
+ if (elapsed !== undefined) parts.push((isZh() ? '已运行 ' : '') + formatDuration(elapsed));
1348
+ const tokens = tokenTotal(
1349
+ s.projectionValues === undefined || s.projectionValues === null
1350
+ ? undefined
1351
+ : s.projectionValues.tokenUsage
1352
+ );
1353
+ if (tokens !== undefined) parts.push(formatTokens(tokens) + ' tok');
1354
+ if (parts.length === 0) parts.push(isZh() ? '正在运行…' : 'running…');
1355
+ return parts.join(' · ');
1356
+ };
1357
+
1358
+ const runningSubagents = () => {
1359
+ try {
1360
+ const snapshot = sessions.list.getSnapshot();
1361
+ if (snapshot === undefined || snapshot === null || snapshot.byId === undefined || snapshot.byId === null) return [];
1362
+ const items = [];
1363
+ for (const key of Object.keys(snapshot.byId)) {
1364
+ const s = snapshot.byId[key];
1365
+ if (s === null || s === undefined) continue;
1366
+ if (s.running !== true || s.origin !== 'subagent') continue;
1367
+ items.push(s);
1368
+ }
1369
+ items.sort((a, b) => startOf(a) - startOf(b));
1370
+ return items;
1371
+ } catch (error) {
1372
+ return [];
1373
+ }
1374
+ };
1375
+
1376
+ const openSubagent = (s) => {
1377
+ try {
1378
+ let address;
1379
+ if (typeof sessions.subagentAddress === 'function') address = sessions.subagentAddress(s.id);
1380
+ if (address !== undefined && address !== null && typeof sessions.openSubagent === 'function') sessions.openSubagent(address);
1381
+ else sessions.open(s.id);
1382
+ } catch (error) {
1383
+ try { sessions.open(s.id); } catch (inner) {}
1384
+ }
1385
+ };
1386
+
1387
+ const STOP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><rect x="1" y="1" width="8" height="8" rx="2"/></svg>';
1388
+
1389
+ const stopTitle = () => (isZh() ? '停止此子代理' : 'Stop this subagent');
1390
+
1391
+ const rows = new Map();
1392
+ const buildRow = (s) => {
1393
+ const item = document.createElement('div');
1394
+ item.className = 'dshome-sa-item';
1395
+ item.setAttribute('role', 'button');
1396
+ item.tabIndex = 0;
1397
+ const sweep = document.createElement('span');
1398
+ sweep.className = 'dshome-sa-sweep';
1399
+ item.appendChild(sweep);
1400
+ const dot = document.createElement('span');
1401
+ dot.className = 'dshome-sa-dot';
1402
+ const body = document.createElement('span');
1403
+ body.className = 'dshome-sa-body';
1404
+ const name = document.createElement('span');
1405
+ name.className = 'dshome-sa-name';
1406
+ const meta = document.createElement('span');
1407
+ meta.className = 'dshome-sa-meta';
1408
+ const bar = document.createElement('span');
1409
+ bar.className = 'dshome-sa-bar';
1410
+ const fill = document.createElement('span');
1411
+ fill.className = 'dshome-sa-bar-fill';
1412
+ bar.appendChild(fill);
1413
+ body.appendChild(name);
1414
+ body.appendChild(meta);
1415
+ body.appendChild(bar);
1416
+ item.appendChild(dot);
1417
+ item.appendChild(body);
1418
+
1419
+ const stopBtn = document.createElement('button');
1420
+ stopBtn.type = 'button';
1421
+ stopBtn.className = 'dshome-sa-stop';
1422
+ stopBtn.innerHTML = STOP_SVG;
1423
+ stopBtn.title = stopTitle();
1424
+ stopBtn.setAttribute('aria-label', stopTitle());
1425
+ stopBtn.addEventListener('click', (event) => stopSubagent(s, stopBtn, event));
1426
+ item.appendChild(stopBtn);
1427
+
1428
+ item.addEventListener('click', () => openSubagent(s));
1429
+ item.addEventListener('keydown', (event) => {
1430
+ if (event.key === 'Enter' || event.key === ' ') {
1431
+ event.preventDefault();
1432
+ openSubagent(s);
1433
+ }
1434
+ });
1435
+ return {
1436
+ root: item,
1437
+ nameEl: name,
1438
+ metaEl: meta,
1439
+ stopEl: stopBtn,
1440
+ sweepEl: sweep,
1441
+ summary: s,
1442
+ phase: 'running',
1443
+ progress: 0,
1444
+ stopStart: 0,
1445
+ completeStart: 0,
1446
+ fillFrom: 0,
1447
+ exitTimer: 0
1448
+ };
1449
+ };
1450
+
1451
+ const deniedTimers = new Set();
1452
+ const flashDenied = (btn) => {
1453
+ btn.classList.add('dshome-sa-stop-denied');
1454
+ const timer = setTimeout(() => {
1455
+ deniedTimers.delete(timer);
1456
+ btn.classList.remove('dshome-sa-stop-denied');
1457
+ }, 1200);
1458
+ deniedTimers.add(timer);
1459
+ };
1460
+
1461
+ const easeOutCubic = (k) => 1 - Math.pow(1 - k, 3);
1462
+ /**
1463
+ * Fake stopping progress: sprint to 80% within the first second,
1464
+ * then crawl asymptotically toward 95% — never 100% on its own;
1465
+ * termination is what completes the bar.
1466
+ */
1467
+ const stoppingProgress = (elapsedSec) => {
1468
+ if (elapsedSec <= 1) return 0.8 * easeOutCubic(elapsedSec);
1469
+ return Math.min(0.95, 0.8 + 0.15 * (1 - Math.exp(-(elapsedSec - 1) / 3)));
1470
+ };
1471
+
1472
+ let sweepRaf = 0;
1473
+ const sweepTick = () => {
1474
+ const now = performance.now();
1475
+ let any = false;
1476
+ for (const row of rows.values()) {
1477
+ if (row.phase === 'stopping') {
1478
+ any = true;
1479
+ row.progress = stoppingProgress((now - row.stopStart) / 1000);
1480
+ row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
1481
+ } else if (row.phase === 'completing') {
1482
+ any = true;
1483
+ const t = (now - row.completeStart) / 1000;
1484
+ if (t < 0.3) {
1485
+ const k = easeOutCubic(Math.min(1, t / 0.3));
1486
+ row.progress = row.fillFrom + (1 - row.fillFrom) * k;
1487
+ row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
1488
+ } else if (t < 0.6) {
1489
+ row.sweepEl.style.width = '100%';
1490
+ } else {
1491
+ row.phase = 'exiting';
1492
+ row.root.classList.add('dshome-sa-item-exit');
1493
+ row.exitTimer = setTimeout(() => {
1494
+ row.root.remove();
1495
+ rows.delete(row.summary.id);
1496
+ syncTicker();
1497
+ }, 240);
1498
+ }
1499
+ }
1500
+ }
1501
+ if (any) sweepRaf = requestAnimationFrame(sweepTick);
1502
+ else sweepRaf = 0;
1503
+ };
1504
+
1505
+ const hasStopping = () => {
1506
+ for (const row of rows.values()) {
1507
+ if (row.phase === 'stopping' || row.phase === 'completing' || row.phase === 'exiting') return true;
1508
+ }
1509
+ return false;
1510
+ };
1511
+
1512
+ const startStopProgress = (row) => {
1513
+ row.phase = 'stopping';
1514
+ row.progress = 0;
1515
+ row.stopStart = performance.now();
1516
+ row.root.classList.add('dshome-sa-stopping');
1517
+ row.stopEl.classList.remove('dshome-sa-stop-busy');
1518
+ row.sweepEl.style.width = '0%';
1519
+ if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
1520
+ };
1521
+
1522
+ /** Undo the optimistic stopping state when the cancel call is rejected. */
1523
+ const rollbackStop = (row, btn) => {
1524
+ if (btn !== undefined && btn !== null) {
1525
+ btn.disabled = false;
1526
+ btn.classList.remove('dshome-sa-stop-busy');
1527
+ flashDenied(btn);
1528
+ }
1529
+ if (row === undefined) return;
1530
+ if (rows.get(row.summary.id) !== row) return;
1531
+ if (row.phase !== 'stopping') return;
1532
+ row.phase = 'running';
1533
+ row.progress = 0;
1534
+ row.root.classList.remove('dshome-sa-stopping');
1535
+ row.sweepEl.style.width = '0%';
1536
+ };
1537
+
1538
+ const beginCompleting = (row) => {
1539
+ row.phase = 'completing';
1540
+ row.completeStart = performance.now();
1541
+ row.fillFrom = row.progress;
1542
+ if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
1543
+ };
1544
+
1545
+ /**
1546
+ * Durable parent/child address for the subagents.interrupt RPC.
1547
+ * The mode comes from the child's own identity projection; a parent id
1548
+ * is required — generic host routing rejects subagent-owned sessions,
1549
+ * so the interrupt must ride the address, not sessions.cancel.
1550
+ */
1551
+ const addressOf = (s) => {
1552
+ const identity = s.projectionValues === undefined || s.projectionValues === null ? undefined : s.projectionValues.subagent;
1553
+ let mode = 'continuable';
1554
+ if (identity !== undefined && identity !== null && (identity.mode === 'one-shot' || identity.mode === 'continuable')) mode = identity.mode;
1555
+ if (typeof s.parentId !== 'string' || s.parentId === '') return undefined;
1556
+ return { parentSessionId: s.parentId, childSessionId: s.id, mode };
1557
+ };
1558
+
1559
+ const stopSubagent = (s, btn, event) => {
1560
+ if (event !== undefined && event !== null) {
1561
+ event.preventDefault();
1562
+ event.stopPropagation();
1563
+ }
1564
+ const row = rows.get(s.id);
1565
+ if (row !== undefined && row.phase !== 'running') return;
1566
+ let face;
1567
+ try {
1568
+ const binding = sessions.binding(s.id);
1569
+ face = binding === undefined || binding === null ? undefined : binding.session;
1570
+ } catch (error) {
1571
+ face = undefined;
1572
+ }
1573
+ const address = addressOf(s);
1574
+ const api = connection === undefined || connection === null ? undefined : connection.api;
1575
+ const canInterrupt = address !== undefined && address.mode === 'continuable' && api !== undefined && api !== null && api.subagents !== undefined && api.subagents !== null && typeof api.subagents.interrupt === 'function';
1576
+ const canFaceCancel = face !== undefined && face !== null && typeof face.cancel === 'function';
1577
+ if (!canInterrupt && !canFaceCancel) {
1578
+ flashDenied(btn);
1579
+ return;
1580
+ }
1581
+ btn.disabled = true;
1582
+ // Optimistic feedback: the red line + sweep start the moment the
1583
+ // button is pressed; a rejected cancel rolls them back.
1584
+ if (row !== undefined) startStopProgress(row);
1585
+ let cancelPromise;
1586
+ try {
1587
+ cancelPromise = canInterrupt
1588
+ ? Promise.resolve(api.subagents.interrupt(address))
1589
+ : Promise.resolve(face.cancel());
1590
+ } catch (error) {
1591
+ rollbackStop(row, btn);
1592
+ return;
1593
+ }
1594
+ cancelPromise.then((result) => {
1595
+ const ok = result !== undefined && result !== null && result.ok === true;
1596
+ if (!ok) rollbackStop(row, btn);
1597
+ }).catch(() => {
1598
+ rollbackStop(row, btn);
1599
+ });
1600
+ };
1601
+
1602
+ let hideTimer = 0;
1603
+ const hide = () => {
1604
+ if (panel.hidden || hideTimer !== 0) return;
1605
+ panel.classList.add('dshome-sa-leave');
1606
+ hideTimer = setTimeout(() => {
1607
+ hideTimer = 0;
1608
+ panel.hidden = true;
1609
+ panel.classList.remove('dshome-sa-leave');
1610
+ }, 240);
1611
+ };
1612
+
1613
+ let tickTimer = 0;
1614
+ const tick = () => {
1615
+ if (panel.hidden) return;
1616
+ for (const row of rows.values()) {
1617
+ if (row.summary.running !== true) continue;
1618
+ row.metaEl.textContent = metaText(row.summary);
1619
+ }
1620
+ };
1621
+ const syncTicker = () => {
1622
+ if (rows.size > 0 && tickTimer === 0) {
1623
+ tickTimer = setInterval(tick, 1000);
1624
+ } else if (rows.size === 0 && tickTimer !== 0) {
1625
+ clearInterval(tickTimer);
1626
+ tickTimer = 0;
1627
+ }
1628
+ };
1629
+
1630
+ let collapsed = false;
1631
+ const updateCollapsed = () => {
1632
+ panel.classList.toggle('dshome-sa-collapsed', collapsed);
1633
+ const label = collapsed ? (isZh() ? '展开' : 'Expand') : (isZh() ? '收起' : 'Collapse');
1634
+ collapseBtn.setAttribute('aria-label', label);
1635
+ collapseBtn.title = label;
1636
+ };
1637
+ collapseBtn.addEventListener('click', () => {
1638
+ collapsed = !collapsed;
1639
+ updateCollapsed();
1640
+ });
1641
+
1642
+ const render = () => {
1643
+ const items = runningSubagents();
1644
+ const stopping = hasStopping();
1645
+ if (items.length === 0 && !stopping) {
1646
+ titleEl.textContent = runningTitle(0);
1647
+ hide();
1648
+ return;
1649
+ }
1650
+ titleEl.textContent = items.length > 0 ? runningTitle(items.length) : (isZh() ? '正在停止子代理…' : 'Stopping subagent…');
1651
+ if (hideTimer !== 0) {
1652
+ clearTimeout(hideTimer);
1653
+ hideTimer = 0;
1654
+ }
1655
+ panel.classList.remove('dshome-sa-leave');
1656
+ panel.hidden = false;
1657
+ const seen = new Set();
1658
+ for (const s of items) {
1659
+ seen.add(s.id);
1660
+ let row = rows.get(s.id);
1661
+ if (row === undefined) {
1662
+ row = buildRow(s);
1663
+ rows.set(s.id, row);
1664
+ listEl.appendChild(row.root);
1665
+ }
1666
+ row.summary = s;
1667
+ const name = s.displayTitle || s.id;
1668
+ row.nameEl.textContent = name;
1669
+ row.root.title = name;
1670
+ row.metaEl.textContent = metaText(s);
1671
+ }
1672
+ for (const entry of Array.from(rows.entries())) {
1673
+ const id = entry[0];
1674
+ const row = entry[1];
1675
+ if (seen.has(id)) continue;
1676
+ if (row.phase === 'stopping') {
1677
+ beginCompleting(row);
1678
+ continue;
1679
+ }
1680
+ if (row.phase === 'completing' || row.phase === 'exiting') continue;
1681
+ row.root.remove();
1682
+ rows.delete(id);
1683
+ }
1684
+ syncTicker();
1685
+ };
1686
+
1687
+ const unsubscribe = sessions.list.subscribe(render);
1688
+ render();
1689
+ updateCollapsed();
1690
+
1691
+ return () => {
1692
+ unsubscribe();
1693
+ if (sweepRaf !== 0) cancelAnimationFrame(sweepRaf);
1694
+ if (tickTimer !== 0) clearInterval(tickTimer);
1695
+ if (hideTimer !== 0) clearTimeout(hideTimer);
1696
+ for (const row of rows.values()) {
1697
+ if (row.exitTimer !== 0) clearTimeout(row.exitTimer);
1698
+ }
1699
+ for (const timer of deniedTimers) clearTimeout(timer);
1700
+ deniedTimers.clear();
1701
+ panel.remove();
1702
+ styleTag.remove();
1703
+ };
1704
+ }
1705
+
1706
+ function LocaleToggle(props) {
1707
+ const locale = props.locale;
1708
+ const [active, setActive] = React.useState('zh');
1709
+
1710
+ React.useEffect(() => {
1711
+ if (locale === undefined) return;
1712
+ const sync = () => {
1713
+ try { setActive(locale.getLocale().active); } catch (e) {}
1714
+ };
1715
+ sync();
1716
+ return locale.subscribe(sync);
1717
+ }, [locale]);
1718
+
1719
+ const pick = (id) => () => {
1720
+ if (locale === undefined) return;
1721
+ try { locale.setLocale(id); } catch (e) {}
1722
+ };
1723
+
1724
+ const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
1725
+
1726
+ return React.createElement('div', { className: 'dshome-locale', role: 'group', 'aria-label': 'Language' },
1727
+ React.createElement('button', { type: 'button', className: isZh ? 'dshome-on' : '', onClick: pick('zh') },
1728
+ React.createElement('span', { className: 'dshome-full' }, '中文'),
1729
+ React.createElement('span', { className: 'dshome-short' }, '中')),
1730
+ React.createElement('button', { type: 'button', className: !isZh ? 'dshome-on' : '', onClick: pick('en') },
1731
+ React.createElement('span', { className: 'dshome-full' }, 'EN'),
1732
+ React.createElement('span', { className: 'dshome-short' }, 'E'))
1733
+ );
1734
+ }
1735
+
1736
+ // ── DSTT (DeepSeekStyleTheme) settings + peak-hour color driver ──────
1737
+ // The host half owns the durable `deepseek-style-theme` settings
1738
+ // namespace; the browser half reads/writes one `mode` enum over this
1739
+ // plugin's private loopback channel — the settings RPC surface only
1740
+ // serves the core's allowlisted namespaces. A shared in-memory mirror
1741
+ // feeds the event-chain clock, which checks the Beijing time once, then
1742
+ // schedules exactly the next boundary switch — vivid red (鲜红) inside a
1743
+ // peak window (Beijing 9:00–12:00, 14:00–18:00), blue or green outside
1744
+ // per mode. One-minute granularity (ceiling) per the user's rule.
1745
+ const DSTT_NS = "deepseek-style-theme";
1746
+ const DSTT_FIELD = "mode";
1747
+ const DSTT_MODES = ["peakvalley-redblue", "peakvalley-redgreen", "always-green", "always-blue"];
1748
+ const DSTT_LABELS = {
1749
+ "peakvalley-redblue": { zh: "峰谷红蓝", en: "Peak R·B" },
1750
+ "peakvalley-redgreen": { zh: "峰谷红绿", en: "Peak R·G" },
1751
+ "always-green": { zh: "常态绿", en: "Always G" },
1752
+ "always-blue": { zh: "常态蓝", en: "Always B" }
1753
+ };
1754
+ /** DSTT private-channel endpoints (host half in lib/index.js). */
1755
+ const DSTT_GET = "dstt.mode.get";
1756
+ const DSTT_SET = "dstt.mode.set";
1757
+ /** Peak windows, [startHour, endHour), judged in Beijing time (UTC+8). */
1758
+ const PEAK_RANGES = [[9, 12], [14, 18]];
1759
+
1760
+ /**
1761
+ * Shared in-memory mirror of the durable DSTT mode. The settings domain's
1762
+ * RPC only serves allowlisted namespaces, so the mode travels over this
1763
+ * plugin's own loopback channel instead; every reader (the color driver,
1764
+ * the settings tab) subscribes to this mirror.
1765
+ */
1766
+ const dsttState = { mode: "peakvalley-redblue", listeners: new Set() };
1767
+ const dsttGetMode = () => dsttState.mode;
1768
+ const dsttSubscribe = (listener) => {
1769
+ dsttState.listeners.add(listener);
1770
+ return () => dsttState.listeners.delete(listener);
1771
+ };
1772
+ const dsttNotify = () => {
1773
+ for (const listener of dsttState.listeners) {
1774
+ try { listener(); } catch (e) {}
1775
+ }
1776
+ };
1777
+ /** Path of this plugin's private host bridge (a webServer prefix route). */
1778
+ const BRIDGE_PATH = '/dshome-open-workspace';
1779
+
1780
+ /**
1781
+ * Call the host bridge. The host half owns a plain `webServer` prefix route
1782
+ * (dsh 0.1.5's connection carrier cannot host this plugin's channel without
1783
+ * tripping cordis' webServer guard), reached with a same-origin fetch so the
1784
+ * browser session authenticates it like any other web-app request. Resolves
1785
+ * to the RpcResult-shaped JSON, throws on a transport failure.
1786
+ */
1787
+ async function bridgeCall(endpoint, payload) {
1788
+ const response = await fetch(BRIDGE_PATH, {
1789
+ method: 'POST',
1790
+ headers: { 'content-type': 'application/json' },
1791
+ body: JSON.stringify({ endpoint: endpoint, payload: payload })
1792
+ });
1793
+ if (!response.ok) throw new Error('bridge http ' + response.status);
1794
+ return await response.json();
1795
+ }
1796
+
1797
+ /** Pull the durable mode from the host half once. */
1798
+ async function dsttSync(ctx) {
1799
+ try {
1800
+ const result = await bridgeCall(DSTT_GET, {});
1801
+ if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
1802
+ dsttState.mode = result.value.mode;
1803
+ dsttNotify();
1804
+ }
1805
+ } catch (error) {}
1806
+ }
1807
+ /** Persist a mode choice through the host half; resolves with success. */
1808
+ async function dsttSetMode(ctx, mode) {
1809
+ if (!DSTT_MODES.includes(mode)) return false;
1810
+ try {
1811
+ const result = await bridgeCall(DSTT_SET, { mode });
1812
+ if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
1813
+ dsttState.mode = result.value.mode;
1814
+ dsttNotify();
1815
+ return true;
1816
+ }
1817
+ } catch (error) {}
1818
+ return false;
1819
+ }
1820
+
1821
+ /**
1822
+ * Current time shifted onto the Beijing clock. Date#getTime() is already a
1823
+ * UTC instant, so Beijing time is exactly `now + 8h` regardless of the
1824
+ * machine's own timezone — `getTimezoneOffset()` must NOT be involved
1825
+ * (adding it double-counts on UTC+8 machines and reads the wrong hour).
1826
+ * Beijing has no DST, so the +8h shift is exact.
1827
+ */
1828
+ function beijingNow() {
1829
+ return new Date(Date.now() + 8 * 3600000);
1830
+ }
1831
+
1832
+ /** Beijing wall-clock HH:MM:SS for logs. */
1833
+ function beijingClock(bj) {
1834
+ const pad = (n) => String(n).padStart(2, '0');
1835
+ return pad(bj.getUTCHours()) + ':' + pad(bj.getUTCMinutes()) + ':' + pad(bj.getUTCSeconds()) + ' (UTC+8)';
1836
+ }
1837
+
1838
+ /**
1839
+ * Round a Beijing time UP to the next whole minute (ceiling on the second
1840
+ * field). One-minute granularity: 12:59:17 reads as 13:00 → valley, and
1841
+ * the moment the wall clock hits 14:00 the red takes over; 13:59:30
1842
+ * already counts as 14:00 (inside the window) because it rounds up.
1843
+ */
1844
+ function ceilToMinute(beijingDate) {
1845
+ const rounded = new Date(beijingDate.getTime());
1846
+ if (rounded.getUTCSeconds() > 0 || rounded.getUTCMilliseconds() > 0) {
1847
+ rounded.setUTCSeconds(0, 0);
1848
+ rounded.setUTCMinutes(rounded.getUTCMinutes() + 1);
1849
+ }
1850
+ return rounded;
1851
+ }
1852
+
1853
+ /** Whether a rounded Beijing minute falls inside a peak window. Weekends
1854
+ * (Beijing Saturday/Sunday) are entirely off-peak. */
1855
+ function isPeakMinute(beijingDate) {
1856
+ const day = beijingDate.getUTCDay();
1857
+ if (day === 0 || day === 6) return false;
1858
+ const hour = beijingDate.getUTCHours();
1859
+ return PEAK_RANGES.some(([from, to]) => hour >= from && hour < to);
1860
+ }
1861
+
1862
+ /** Next Beijing wall-clock occurrence of `hour` (today if still ahead, else tomorrow). */
1863
+ function nextBoundary(beijingDate, hour) {
1864
+ const next = new Date(beijingDate.getTime());
1865
+ next.setUTCHours(hour, 0, 0, 0);
1866
+ if (next.getTime() <= beijingDate.getTime()) next.setUTCDate(next.getUTCDate() + 1);
1867
+ return next;
1868
+ }
1869
+
1870
+ /** The boundary that ends the peak window `beijingDate` is inside, or null. */
1871
+ function endOfWindow(beijingDate) {
1872
+ const hour = beijingDate.getUTCHours();
1873
+ for (const [from, to] of PEAK_RANGES) {
1874
+ if (hour >= from && hour < to) return nextBoundary(beijingDate, to);
1875
+ }
1876
+ return null;
1877
+ }
1878
+
1879
+ /** The earliest future peak-window start, or null if none (defensive). */
1880
+ function nextStart(beijingDate) {
1881
+ const candidates = PEAK_RANGES
1882
+ .map(([from]) => nextBoundary(beijingDate, from))
1883
+ .filter((d) => d.getTime() > beijingDate.getTime())
1884
+ .sort((a, b) => a - b);
1885
+ return candidates.length === 0 ? null : candidates[0];
1886
+ }
1887
+
1888
+ /** Color-change listeners (particles, UI re-render on switch). */
1889
+ const colorListeners = new Set();
1890
+ const subscribeColor = (fn) => {
1891
+ colorListeners.add(fn);
1892
+ return () => colorListeners.delete(fn);
1893
+ };
1894
+ const notifyColor = (color) => {
1895
+ for (const fn of colorListeners) {
1896
+ try { fn(color); } catch (e) {}
1897
+ }
1898
+ };
1899
+ const isTimeSliced = (mode) => mode === "peakvalley-redblue" || mode === "peakvalley-redgreen";
1900
+ const colorFor = (mode, peak) => {
1901
+ if (mode === "always-blue") return "blue";
1902
+ if (mode === "always-green") return "green";
1903
+ return peak ? "red" : (mode === "peakvalley-redgreen" ? "green" : "blue");
1904
+ };
1905
+
1906
+ /**
1907
+ * Color driver — event chain, no polling. On start it checks the
1908
+ * Beijing clock once, then schedules exactly the next boundary switch:
1909
+ * inside a window → vivid red now, record the delta to the window end,
1910
+ * flip back to the valley color when it elapses; outside → valley color,
1911
+ * flip to red at the next window start. Toggling the DSTT mode
1912
+ * re-anchors immediately; closing the feature tears every timer down. A
1913
+ * `visibilitychange` re-anchors the chain from the live clock in case a
1914
+ * throttled background tab drifted.
1915
+ */
1916
+ function watchPeakHour(ctx) {
1917
+ let chainTimer = 0;
1918
+ let stopped = false;
1919
+ let unsubscribe = null;
1920
+
1921
+ const applyColor = () => {
1922
+ if (stopped) return;
1923
+ const mode = dsttGetMode();
1924
+ const peak = isTimeSliced(mode) && isPeakMinute(ceilToMinute(beijingNow()));
1925
+ const color = colorFor(mode, peak);
1926
+ if (document.body.getAttribute('data-dshome-color') !== color) {
1927
+ document.body.setAttribute('data-dshome-color', color);
1928
+ console.info('[' + PLUGIN_ID + '] color -> ' + color + (peak ? ' (peak)' : ' (valley)') + ' @ ' + beijingClock(beijingNow()));
1929
+ notifyColor(color);
1930
+ }
1931
+ };
1932
+
1933
+ const schedule = (ms) => {
1934
+ if (chainTimer !== 0) {
1935
+ clearTimeout(chainTimer);
1936
+ chainTimer = 0;
1937
+ }
1938
+ // setTimeout caps near 24.8 days; the longest span here (to
1939
+ // tomorrow's first window start) is far below that. The +500ms
1940
+ // margin re-anchors cleanly across the minute boundary.
1941
+ chainTimer = setTimeout(anchor, Math.max(0, ms) + 500);
1942
+ };
1943
+
1944
+ /** The single entry point: read the mode + Beijing clock, decide, schedule. */
1945
+ const anchor = () => {
1946
+ if (stopped) return;
1947
+ applyColor();
1948
+ const mode = dsttGetMode();
1949
+ if (!isTimeSliced(mode)) return;
1950
+ const now = ceilToMinute(beijingNow());
1951
+ const end = endOfWindow(now);
1952
+ if (end !== null) {
1953
+ schedule(end.getTime() - now.getTime());
1954
+ } else {
1955
+ const start = nextStart(now);
1956
+ if (start !== null) schedule(start.getTime() - now.getTime());
1957
+ }
1958
+ };
1959
+
1960
+ const onVisibility = () => {
1961
+ if (!document.hidden) anchor();
1962
+ };
1963
+ document.addEventListener('visibilitychange', onVisibility);
1964
+
1965
+ anchor();
1966
+
1967
+ try { unsubscribe = dsttSubscribe(anchor); } catch (error) { unsubscribe = null; }
1968
+
1969
+ return () => {
1970
+ stopped = true;
1971
+ if (chainTimer !== 0) clearTimeout(chainTimer);
1972
+ if (unsubscribe !== null) unsubscribe();
1973
+ document.removeEventListener('visibilitychange', onVisibility);
1974
+ };
1975
+ }
1976
+
1977
+ /**
1978
+ * DSTT settings tab (title: DSTT). Renders the four-way mode picker plus
1979
+ * a live status line. Reads/writes the durable namespace through the
1980
+ * host-side RPC mirror; every subscription and timer is owned by the
1981
+ * component.
1982
+ */
1983
+ function DsttSection(props) {
1984
+ const dstt = props.dstt;
1985
+ const locale = props.locale;
1986
+ const [mode, setMode] = React.useState(() => (dstt === undefined ? 'peakvalley-redblue' : dstt.mode()));
1987
+ const [active, setActive] = React.useState('zh');
1988
+ const [, force] = React.useReducer((x) => x + 1, 0);
1989
+
1990
+ React.useEffect(() => {
1991
+ if (locale === undefined) return;
1992
+ const sync = () => {
1993
+ try { setActive(locale.getLocale().active); } catch (e) {}
1994
+ };
1995
+ sync();
1996
+ return locale.subscribe(sync);
1997
+ }, [locale]);
1998
+
1999
+ React.useEffect(() => {
2000
+ if (dstt === undefined) return () => {};
2001
+ const refresh = () => {
2002
+ try { setMode(dstt.mode()); } catch (e) {}
2003
+ force();
2004
+ };
2005
+ let unsubscribe = null;
2006
+ try { unsubscribe = dstt.subscribe(refresh); } catch (e) { unsubscribe = null; }
2007
+ const unsubscribeColor = subscribeColor(() => force());
2008
+ refresh();
2009
+ const timer = setInterval(() => {
2010
+ force();
2011
+ }, 30000);
2012
+ return () => {
2013
+ clearInterval(timer);
2014
+ if (unsubscribe !== null) unsubscribe();
2015
+ unsubscribeColor();
2016
+ };
2017
+ }, [dstt]);
2018
+
2019
+ const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
2020
+ const nowMin = ceilToMinute(beijingNow());
2021
+ const peakNow = isPeakMinute(nowMin);
2022
+ const end = peakNow ? endOfWindow(nowMin) : null;
2023
+ const deltaMins = end === null ? 0 : Math.max(0, Math.round((end.getTime() - nowMin.getTime()) / 60000));
2024
+ const deltaText = deltaMins <= 0
2025
+ ? ''
2026
+ : isZh
2027
+ ? (deltaMins >= 60 ? Math.floor(deltaMins / 60) + '小时' + String(deltaMins % 60).padStart(2, '0') + '分' : deltaMins + '分')
2028
+ : (deltaMins >= 60 ? Math.floor(deltaMins / 60) + 'h ' + (deltaMins % 60) + 'm' : deltaMins + 'm');
2029
+ const colorNow = document.body.getAttribute('data-dshome-color') || 'green';
2030
+ const colorName = colorNow === 'red' ? (isZh ? '鲜红' : 'vivid red') : colorNow === 'blue' ? (isZh ? '蓝色' : 'blue') : (isZh ? '绿色' : 'green');
2031
+ const timeSliced = isTimeSliced(mode);
2032
+ const status = mode === 'always-blue'
2033
+ ? (isZh ? '固定蓝色模式' : 'Fixed blue')
2034
+ : mode === 'always-green'
2035
+ ? (isZh ? '固定绿色模式' : 'Fixed green')
2036
+ : peakNow
2037
+ ? (isZh ? '高峰时段 · 鲜红' + (deltaText === '' ? '' : ' · 还剩 ' + deltaText) : 'Peak hours · vivid red' + (deltaText === '' ? '' : ' · ' + deltaText + ' left'))
2038
+ : (isZh ? '谷时段 · ' + (mode === 'peakvalley-redgreen' ? '绿色' : '蓝色') : 'Off-peak · ' + (mode === 'peakvalley-redgreen' ? 'green' : 'blue'));
2039
+
2040
+ const pickMode = (next) => {
2041
+ if (dstt === undefined || next === mode) return;
2042
+ dstt.set(next);
2043
+ };
2044
+
2045
+ return React.createElement('div', { className: 'dshome-dstt' },
2046
+ React.createElement('h3', null,
2047
+ 'DeepSeekStyleTheme',
2048
+ React.createElement('span', { className: 'dshome-dstt-tag' }, 'DSTT')),
2049
+ React.createElement('p', null, isZh
2050
+ ? '谷时段默认绿色;高峰时段(北京时间 9:00–12:00、14:00–18:00,周六周日全天非高峰)自动切换为鲜红提示。峰谷模式可选谷色为蓝或绿,常态模式全天固定。'
2051
+ : 'Off-peak green; peak hours (Beijing 9:00–12:00, 14:00–18:00, all day off-peak on weekends) switch to vivid red. Peak/valley modes pick the valley color (blue or green); always modes stay fixed all day.'),
2052
+ React.createElement('div', { className: 'dshome-dstt-row' },
2053
+ React.createElement('div', { className: 'dshome-dstt-row-body' },
2054
+ React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '主题模式' : 'Theme mode'),
2055
+ React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
2056
+ ? '峰谷红蓝 / 峰谷红绿:高峰鲜红、谷时段蓝 / 绿;常态绿 / 常态蓝:全天固定'
2057
+ : 'Peak R·B / Peak R·G: red at peak, blue/green at valley; Always G/B: fixed all day')),
2058
+ React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '主题模式' : 'Theme mode' },
2059
+ DSTT_MODES.map((m) => React.createElement('button', {
2060
+ key: m,
2061
+ type: 'button',
2062
+ role: 'radio',
2063
+ 'aria-checked': mode === m,
2064
+ className: 'dshome-dstt-seg-btn' + (mode === m ? ' dshome-on' : ''),
2065
+ onClick: () => pickMode(m)
2066
+ }, isZh ? DSTT_LABELS[m].zh : DSTT_LABELS[m].en)))),
2067
+ React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
2068
+ '当前:' + colorName + ' · ' + status));
2069
+ }
2070
+
2071
+ function apply(ctx) {
2072
+ // A `document.querySelector('[data-dsh-probe]')` cleanup used to run
2073
+ // here. Removing an element selected by a generic attribute deletes
2074
+ // whatever else happens to use that attribute, so it is gone: a probe
2075
+ // owns the nodes it creates and removes them itself.
2076
+
2077
+ // Core styles (ours only) are always injected.
2078
+ ctx.effect(() => injectStyle(CORE_CSS, 'theme.css'), 'deepseek-style-theme: core css');
2079
+ ctx.effect(() => injectStyle(ACTIONS_CSS, 'actions.css'), 'deepseek-style-theme: actions css');
2080
+
2081
+ const theme = ctx.get('theme');
2082
+
2083
+ // DSTT: the durable mode travels over this plugin's private loopback
2084
+ // channel (the settings RPC surface only serves allowlisted
2085
+ // namespaces). Pull it once, then let the color driver and the
2086
+ // settings tab share the in-memory mirror.
2087
+ ctx.effect(() => {
2088
+ dsttSync(ctx);
2089
+ }, 'deepseek-style-theme: dstt sync');
2090
+ ctx.effect(() => watchPeakHour(ctx), 'deepseek-style-theme: color driver');
2091
+
2092
+ // Dark marker: owned attribute driven by the theme service, never by
2093
+ // the product's attribute name. Must precede the patch blocks so the
2094
+ // dark selectors match from the first paint.
2095
+ ctx.effect(() => createDarkSync(ctx, theme), 'deepseek-style-theme: dark sync');
2096
+
2097
+ // Guarded product-surface patches: each block activates only while
2098
+ // its anchor class exists in the DOM.
2099
+ for (const block of PATCH_BLOCKS) {
2100
+ ctx.effect(() => installGuarded(block.anchor, block.css), 'deepseek-style-theme: patch ' + block.anchor);
2101
+ }
2102
+
2103
+ if (theme !== undefined) {
2104
+ const disposeTokens = theme.overrideTokens('deepseek-style-theme', {
2105
+ '--dsw-alias-bg-base': { light: '#f6f7fb', dark: '#0a1a3a' },
2106
+ '--dsw-alias-bg-layer-1': { light: 'rgba(255,255,255,0.72)', dark: 'rgba(15,32,66,0.72)' },
2107
+ '--dsw-alias-bg-layer-2': { light: 'rgba(255,255,255,0.85)', dark: 'rgba(21,42,86,0.8)' },
2108
+ '--dsw-alias-bg-overlay': { light: 'rgba(255,255,255,0.92)', dark: 'rgba(10,26,58,0.94)' },
2109
+ '--dsw-alias-border-l1': { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.08)' },
2110
+ '--dsw-alias-border-l2': { light: 'rgba(0,0,0,0.1)', dark: 'rgba(255,255,255,0.14)' },
2111
+ '--dsw-alias-brand-primary': { light: 'var(--ds-brand)', dark: 'color-mix(in srgb,var(--ds-brand) 72%,#fff)' },
2112
+ '--dsw-alias-label-primary': { light: '#152443', dark: '#eef2ff' },
2113
+ '--dsw-alias-label-secondary': { light: '#3c4a63', dark: '#a9b8d8' },
2114
+ '--dsw-alias-state-error-primary': { light: '#ef4444', dark: '#ff6b6b' },
2115
+ '--dsw-alias-state-success-primary': { light: '#22c55e', dark: '#4ed17e' },
2116
+ '--dsw-alias-state-warn-primary': { light: '#f59e0b', dark: '#f7ad31' },
2117
+ '--dsw-specific-sidebar-fill': { light: 'rgba(255,255,255,0.5)', dark: 'rgba(10,26,58,0.48)' }
2118
+ });
2119
+ ctx.effect(() => disposeTokens);
2120
+ }
2121
+
2122
+ ctx.effect(() => {
2123
+ try { return startParticles(); } catch (error) { return () => {}; }
2124
+ });
2125
+ ctx.effect(() => {
2126
+ try { return watchSettingsSection(); } catch (error) { return () => {}; }
2127
+ });
2128
+ ctx.effect(() => {
2129
+ try { return watchBrandLink(); } catch (error) { return () => {}; }
2130
+ });
2131
+ ctx.effect(() => {
2132
+ try { return watchHeaderInteraction(); } catch (error) { return () => {}; }
2133
+ });
2134
+ ctx.effect(() => {
2135
+ try { return watchWorkspaceMenu(ctx); } catch (error) { return () => {}; }
2136
+ }, 'deepseek-style-theme: workspace open menu');
2137
+ ctx.effect(() => {
2138
+ try { return watchFileCardMenu(ctx); } catch (error) { return () => {}; }
2139
+ }, 'deepseek-style-theme: delivered file card menu');
2140
+ ctx.effect(() => {
2141
+ try { return watchRunningSubagents(ctx); } catch (error) { return () => {}; }
2142
+ }, 'deepseek-style-theme: running subagents panel');
2143
+
2144
+ const slots = ctx.get('slots');
2145
+ if (slots !== undefined) {
2146
+ slots.inject('conversation.session.header.utilities', () => slots.register(
2147
+ { name: 'conversation.session.header.utilities', id: 'deepseek-locale', order: 90 },
2148
+ () => React.createElement(LocaleToggle, { locale: ctx.get('locale') })
2149
+ ));
2150
+ slots.inject('settings.section', () => slots.register({
2151
+ name: 'settings.section',
2152
+ id: 'dstt',
2153
+ order: 100,
2154
+ label: () => 'DSTT',
2155
+ inject: () => ({
2156
+ dstt: {
2157
+ mode: dsttGetMode,
2158
+ set: (mode) => dsttSetMode(ctx, mode),
2159
+ subscribe: dsttSubscribe
2160
+ },
2161
+ locale: ctx.get('locale')
2162
+ })
2163
+ }, DsttSection));
2164
+ }
2165
+ }
2166
+
2167
+ exports.apply = apply;
2168
+ exports.inject = ['theme', 'slots', 'locale', 'workspaces', 'connection'];
2169
+ return module.exports;
2170
+ };
2171
+
2172
+ // Both ids, same factory. The package moved from
2173
+ // @dsh-external/dsh-deepseek-style-theme to the unscoped
2174
+ // dsh-deepseek-style-theme, and the loader asks a bundle to register under
2175
+ // whichever id the installed copy carries -- while this rolled-back file
2176
+ // still carries the old literal internally. Registering both, each guarded,
2177
+ // means either host finds the id it wants and the other entry is simply
2178
+ // never requested.
2179
+ try { window.__ModuleLoader__.load({ id: "dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory }); } catch (error) {}
2180
+ try { window.__ModuleLoader__.load({ id: "@dsh-external/dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory }); } catch (error) {}