dsh-milestone 0.6.2 → 0.6.3

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 (3) hide show
  1. package/README.md +12 -4
  2. package/lib/client.js +679 -170
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="./assets/logo.svg" alt="dsh-milestone" width="112">
3
+ <img src="https://raw.githubusercontent.com/SnowCrescenter-tech/dsh-milestone/main/assets/logo.svg" alt="dsh-milestone" width="112">
4
4
 
5
5
  # dsh-milestone
6
6
 
@@ -25,7 +25,7 @@
25
25
  - 右侧挂一条**圆点时间线**:一个提问一个圆点,悬停看内容,点击瞬间跳转——长对话的「导航地图」。
26
26
  - 官方 slot 机制挂载,不修改 harness 源码,装完即用。
27
27
 
28
- <img src="./assets/demo.svg" alt="dsh-milestone 效果示意图" width="100%">
28
+ <img src="https://raw.githubusercontent.com/SnowCrescenter-tech/dsh-milestone/main/assets/demo.svg" alt="dsh-milestone 效果示意图" width="100%">
29
29
 
30
30
  ## 快速开始
31
31
 
@@ -63,7 +63,7 @@ npx @deepseek-ai/dsh web # → http://127.0.0.1:3080
63
63
  | 个性化与效率 | 键盘导航:全程不用鼠标 | ↑↓ 移动 · Enter 跳转 · Home/End 首尾 |
64
64
  | 个性化与效率 | turn 分组折叠:按轮次成组,长轮次折成一条 | |
65
65
  | 个性化与效率 | 复制与 fork:一键复制提问全文 / 从此处分支 | |
66
- | 个性化与效率 | 聚焦模式:淡化 thinking 区块,悬停或展开恢复 | |
66
+ | 个性化与效率 | 聚焦模式:淡化 / 折叠思考与工具调用,强度可调、自由搭配 | |
67
67
  | 个性化与效率 | 折叠工具栏:功能键默认收起,箭头展开;设置菜单可把常用键钉到折叠外(持久化 + 恢复默认) | |
68
68
  | 个性化与效率 | 个性化外观:强调色 / 图标圆点大小 / 距侧边距离 / 位置左右(设置内即调即存) | |
69
69
  | 个性化与效率 | 语言切换:跟随系统 / 中文 / English,设置内一键切换 | |
@@ -164,7 +164,15 @@ shell.overlay (root scope)
164
164
  ## 更新日志
165
165
 
166
166
  <details>
167
- <summary>v0.6.2 / v0.6.1 / v0.6.0(点击展开)</summary>
167
+ <summary>v0.6.3 / v0.6.2 / v0.6.1 / v0.6.0(点击展开)</summary>
168
+
169
+ **v0.6.3** · 聚焦高级设置 + 设置模态重设计 · 363 项测试
170
+
171
+ - **聚焦高级设置**:设置内「聚焦」可展开——自定义淡化/折叠 think 推理区与工具调用卡片,淡化强度 20%–80% 可调(稳定选择器 `[data-variant="think"]` / `[data-chat-call-id]`),搭配持久化。
172
+ - **设置模态重设计**:功能悬停描述改为**就近 tip**(行旁悬浮,不再有大块常驻描述区);恢复表头说明(「开启后,折叠时仍显示在箭头旁」);个性化收进**可展开块**(默认折叠,头部实时摘要);间距/圆角/强调色统一,修复行 hover 洗色失效。
173
+ - README 图片改绝对 URL(npm 渲染器不重写相对路径),npm 页 logo/演示图恢复正常。
174
+
175
+ > [GitHub Release v0.6.3](https://github.com/SnowCrescenter-tech/dsh-milestone/releases/tag/v0.6.3)
168
176
 
169
177
  **v0.6.2** · 设置模态化 + 个性化 + 语言切换 · 346 项测试
170
178
 
package/lib/client.js CHANGED
@@ -682,6 +682,8 @@ window.__ModuleLoader__.load({
682
682
  "settings.title": "设置",
683
683
  /** Per-feature toggle label inside the settings menu: keep visible while collapsed. */
684
684
  "settings.pin": "在折叠外显示",
685
+ /** Settings: feature-section hint explaining what the pin switch does. */
686
+ "settings.pin.hint": "开启后,工具栏折叠时该功能仍显示在箭头旁",
685
687
  /** Settings action that clears every pinned feature. */
686
688
  "settings.reset": "恢复默认",
687
689
  /** Settings footer heading above the project links. */
@@ -702,6 +704,16 @@ window.__ModuleLoader__.load({
702
704
  "settings.section.features": "功能与快捷区",
703
705
  /** Settings modal: section heading for the personalization controls. */
704
706
  "settings.section.personal": "个性化",
707
+ /** Settings modal: section heading for the focus-mode controls (0.6.3). */
708
+ "settings.section.focus": "聚焦",
709
+ /** Settings: personalization-section hint shown inside the expanded block. */
710
+ "settings.personal.hint": "圆点、强调色与位置,即调即存",
711
+ /** Settings: aria-label on the personalization block toggle while COLLAPSED. */
712
+ "settings.personal.expand": "展开个性化设置",
713
+ /** Settings: aria-label on the personalization block toggle while EXPANDED. */
714
+ "settings.personal.collapse": "收起个性化设置",
715
+ /** Settings: live value summary on the collapsed personalization header. */
716
+ "settings.personal.summary": "{accent} · 图标 {icon}px · {side}",
705
717
  /** Settings: hover description — in-rail search. */
706
718
  "settings.desc.search": "按完整消息内容过滤并跳转到对应消息",
707
719
  /** Settings: hover description — all-prompts list. */
@@ -730,6 +742,30 @@ window.__ModuleLoader__.load({
730
742
  "settings.side.left": "左侧",
731
743
  /** Settings personalization: side radio — hug the right edge. */
732
744
  "settings.side.right": "右侧",
745
+ /** Settings: focus block — hint shown inside the expanded block. */
746
+ "settings.focus.hint": "这些选项自由组合成你的「聚焦搭配」;总开关仍是工具栏的眼睛按钮",
747
+ /** Settings: aria-label on the focus block toggle while COLLAPSED. */
748
+ "settings.focus.expand": "展开聚焦设置",
749
+ /** Settings: aria-label on the focus block toggle while EXPANDED. */
750
+ "settings.focus.collapse": "收起聚焦设置",
751
+ /** Settings: focus option — dim the think reasoning disclosures. */
752
+ "settings.focus.dimThink": "淡化 think 推理区",
753
+ /** Settings: focus option — dim the tool-call cards. */
754
+ "settings.focus.dimTools": "淡化工具调用卡片",
755
+ /** Settings: focus option — compress think disclosures to a hover strip. */
756
+ "settings.focus.collapseThink": "折叠 think",
757
+ /** Settings: focus option — dim strength slider label. */
758
+ "settings.focus.opacity": "淡化强度",
759
+ /** Settings: live value summary on the collapsed focus header. */
760
+ "settings.focus.summary": "{opts} · 强度 {opacity}%",
761
+ /** Settings: focus summary — short label for the think-dim option. */
762
+ "settings.focus.summary.think": "think 淡化",
763
+ /** Settings: focus summary — short label for the tool-dim option. */
764
+ "settings.focus.summary.tools": "工具淡化",
765
+ /** Settings: focus summary — short label for the think-collapse option. */
766
+ "settings.focus.summary.collapse": "折叠 think",
767
+ /** Settings: focus summary — placeholder when every option is off. */
768
+ "settings.focus.summary.none": "未启用",
733
769
  /** B4 update-check: toolbar button label + title/aria-label. */
734
770
  "update.check": "检查更新",
735
771
  /** B4 update-check: popover title. */
@@ -808,6 +844,7 @@ window.__ModuleLoader__.load({
808
844
  "toolbar.settings.close": "Close settings",
809
845
  "settings.title": "Settings",
810
846
  "settings.pin": "Show outside collapse",
847
+ "settings.pin.hint": "When on, the feature stays beside the arrow while the toolbar is folded",
811
848
  "settings.reset": "Restore defaults",
812
849
  "settings.support": "Support us",
813
850
  "settings.repo": "GitHub repo",
@@ -818,6 +855,11 @@ window.__ModuleLoader__.load({
818
855
  "settings.close": "Close",
819
856
  "settings.section.features": "Features & Shortcuts",
820
857
  "settings.section.personal": "Personalization",
858
+ "settings.section.focus": "Focus",
859
+ "settings.personal.hint": "Dot size, accent color, and position — saved as you adjust",
860
+ "settings.personal.expand": "Expand personalization",
861
+ "settings.personal.collapse": "Collapse personalization",
862
+ "settings.personal.summary": "{accent} · Icon {icon}px · {side}",
821
863
  "settings.desc.search": "Filter by full message text and jump to the match",
822
864
  "settings.desc.list": "Overview of every prompt in this session",
823
865
  "settings.desc.sessionSearch": "Search across all sessions",
@@ -832,6 +874,18 @@ window.__ModuleLoader__.load({
832
874
  "settings.side": "Position",
833
875
  "settings.side.left": "Left",
834
876
  "settings.side.right": "Right",
877
+ "settings.focus.hint": "Combine these options into your own focus recipe; the eye button on the toolbar stays the master switch",
878
+ "settings.focus.expand": "Expand focus settings",
879
+ "settings.focus.collapse": "Collapse focus settings",
880
+ "settings.focus.dimThink": "Dim think reasoning",
881
+ "settings.focus.dimTools": "Dim tool call cards",
882
+ "settings.focus.collapseThink": "Collapse think",
883
+ "settings.focus.opacity": "Dim strength",
884
+ "settings.focus.summary": "{opts} · Strength {opacity}%",
885
+ "settings.focus.summary.think": "Think dim",
886
+ "settings.focus.summary.tools": "Tools dim",
887
+ "settings.focus.summary.collapse": "Think collapse",
888
+ "settings.focus.summary.none": "Off",
835
889
  "update.check": "Check updates",
836
890
  "update.title": "Update check",
837
891
  "update.current": "Current version",
@@ -1739,11 +1793,15 @@ window.__ModuleLoader__.load({
1739
1793
  * JSON object:
1740
1794
  *
1741
1795
  * { "pinned": string[], "accent": "#rrggbb", "iconSize": number,
1742
- * "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en" }
1796
+ * "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en",
1797
+ * "focus": { "dimThink": boolean, "dimTools": boolean,
1798
+ * "collapseThink": boolean, "opacity": number } }
1743
1799
  *
1744
1800
  * Backward compatibility: the pre-personalization blob `{ "pinned": string[] }`
1745
1801
  * (and an entirely absent value) parses to the DEFAULT prefs with the new
1746
- * fields at their defaults — old users keep their pins untouched.
1802
+ * fields at their defaults — old users keep their pins untouched. The same
1803
+ * rule covers the `focus` object: a blob stored before 0.6.3 (no `focus`
1804
+ * field) gains the default focus mix.
1747
1805
  *
1748
1806
  * All reads are sanitized per field:
1749
1807
  * - `pinned`: whitelisted ids only (`TOOLBAR_PIN_IDS`), duplicates dropped,
@@ -1752,7 +1810,10 @@ window.__ModuleLoader__.load({
1752
1810
  * back to the default blue;
1753
1811
  * - `iconSize` / `inset`: finite numbers snapped to the slider step
1754
1812
  * (even values) and clamped to the slider range;
1755
- * - `side`: exactly `'left'` or `'right'`.
1813
+ * - `side`: exactly `'left'` or `'right'`;
1814
+ * - `focus`: three booleans (`dimThink` / `dimTools` / `collapseThink`)
1815
+ * defaulting to `true` / `false` / `false`, plus the dim `opacity`
1816
+ * snapped to the 0.1 step and clamped to [0.2, 0.8].
1756
1817
  *
1757
1818
  * The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
1758
1819
  * dependency-free and unit-testable; MilestoneRail's feature registry keys
@@ -1776,14 +1837,27 @@ window.__ModuleLoader__.load({
1776
1837
  "updateCheck",
1777
1838
  "settings"
1778
1839
  ];
1840
+ /** The default accent (the classic milestone blue). */
1841
+ const DEFAULT_ACCENT = "#4d7cfd";
1842
+ /** Slider domain for the focus dim strength (0.2 = 20% .. 0.8 = 80%). */
1843
+ const FOCUS_OPACITY_MIN = .2;
1844
+ const FOCUS_OPACITY_MAX = .8;
1845
+ /** The canonical default focus mix (dim think at 40%; tools/collapse off). */
1846
+ const DEFAULT_FOCUS_PREFS = {
1847
+ dimThink: true,
1848
+ dimTools: false,
1849
+ collapseThink: false,
1850
+ opacity: .4
1851
+ };
1779
1852
  /** The canonical default prefs ("恢复默认" target; also the read fallback). */
1780
1853
  const DEFAULT_PREFS = {
1781
1854
  pinned: [],
1782
- accent: "#4d7cfd",
1855
+ accent: DEFAULT_ACCENT,
1783
1856
  iconSize: 28,
1784
1857
  inset: 14,
1785
1858
  side: "right",
1786
- locale: "system"
1859
+ locale: "system",
1860
+ focus: { ...DEFAULT_FOCUS_PREFS }
1787
1861
  };
1788
1862
  /** Type guard for registry ids — unknown strings never survive a parse. */
1789
1863
  function isToolbarPinId(id) {
@@ -1811,11 +1885,36 @@ window.__ModuleLoader__.load({
1811
1885
  const snapped = Math.round(Math.min(max, Math.max(min, value)) / step) * step;
1812
1886
  return Math.min(max, Math.max(min, snapped));
1813
1887
  }
1888
+ /** Boolean sanitizer for the focus mix flags: only `true`/`false` survive. */
1889
+ function sanitizeBoolean(value, fallback) {
1890
+ return typeof value === "boolean" ? value : fallback;
1891
+ }
1892
+ /**
1893
+ * Focus-mix sanitizer: `dimTools`/`collapseThink` default off, `dimThink`
1894
+ * defaults ON (the classic pre-0.6.3 behavior), and `opacity` snaps to the
1895
+ * 0.1 step inside [0.2, 0.8]. Snapping works in tenths (step × 10 = integer)
1896
+ * instead of a raw division so hand-edited floats like `0.30000000000000004`
1897
+ * always converge on exactly `0.3`.
1898
+ */
1899
+ function sanitizeFocus(raw) {
1900
+ if (typeof raw !== "object" || raw === null) return { ...DEFAULT_FOCUS_PREFS };
1901
+ const { dimThink, dimTools, collapseThink, opacity } = raw;
1902
+ const clamped = typeof opacity === "number" && Number.isFinite(opacity) ? Math.min(FOCUS_OPACITY_MAX, Math.max(FOCUS_OPACITY_MIN, opacity)) : DEFAULT_FOCUS_PREFS.opacity;
1903
+ const tenths = Math.round(clamped * 10);
1904
+ const snapped = Math.min(FOCUS_OPACITY_MAX, Math.max(FOCUS_OPACITY_MIN, tenths / 10));
1905
+ return {
1906
+ dimThink: sanitizeBoolean(dimThink, DEFAULT_FOCUS_PREFS.dimThink),
1907
+ dimTools: sanitizeBoolean(dimTools, DEFAULT_FOCUS_PREFS.dimTools),
1908
+ collapseThink: sanitizeBoolean(collapseThink, DEFAULT_FOCUS_PREFS.collapseThink),
1909
+ opacity: snapped
1910
+ };
1911
+ }
1814
1912
  /**
1815
1913
  * Parse + sanitize the raw persisted blob: `null` (nothing stored), invalid
1816
1914
  * JSON, or a non-object shape all degrade to the DEFAULT prefs. Each field is
1817
1915
  * sanitized independently, so a half-corrupt blob keeps its valid parts
1818
- * (e.g. an old `{pinned}`-only blob gains the default accent/size/inset/side).
1916
+ * (e.g. an old `{pinned}`-only blob gains the default accent/size/inset/side
1917
+ * AND the default focus mix).
1819
1918
  */
1820
1919
  function parsePrefs(raw) {
1821
1920
  if (raw === null) return { ...DEFAULT_PREFS };
@@ -1826,14 +1925,15 @@ window.__ModuleLoader__.load({
1826
1925
  return { ...DEFAULT_PREFS };
1827
1926
  }
1828
1927
  if (typeof parsed !== "object" || parsed === null) return { ...DEFAULT_PREFS };
1829
- const { pinned, accent, iconSize, inset, side, locale } = parsed;
1928
+ const { pinned, accent, iconSize, inset, side, locale, focus } = parsed;
1830
1929
  return {
1831
1930
  pinned: sanitizePinned(pinned),
1832
1931
  accent: typeof accent === "string" && isHexColor(accent) ? accent.toLowerCase() : DEFAULT_PREFS.accent,
1833
1932
  iconSize: clampStep(iconSize, 20, 36, 2, DEFAULT_PREFS.iconSize),
1834
1933
  inset: clampStep(inset, 0, 40, 2, DEFAULT_PREFS.inset),
1835
1934
  side: side === "left" || side === "right" ? side : DEFAULT_PREFS.side,
1836
- locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale
1935
+ locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale,
1936
+ focus: sanitizeFocus(focus)
1837
1937
  };
1838
1938
  }
1839
1939
  /**
@@ -2179,7 +2279,7 @@ window.__ModuleLoader__.load({
2179
2279
  * Installed plugin version. Injected at build time as
2180
2280
  * `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
2181
2281
  */
2182
- const PLUGIN_VERSION = "0.6.2";
2282
+ const PLUGIN_VERSION = "0.6.3";
2183
2283
  //#endregion
2184
2284
  //#region src/client/MilestoneRail.tsx
2185
2285
  /**
@@ -2266,39 +2366,176 @@ window.__ModuleLoader__.load({
2266
2366
  /** Tooltip anchor width: its maxWidth cap, so a tooltip never overlaps the rail. */
2267
2367
  const TOOLTIP_ANCHOR_WIDTH = 300;
2268
2368
  /**
2269
- * P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
2270
- * conversation reads cleaner. The rule targets the stable, un-hashed
2271
- * `data-variant="think"` attribute on the thinking-block ROOT (the harness
2272
- * renders it as `data-variant="think"` with `data-state="running|ok"`), so an
2273
- * overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
2274
- * opening it, `[data-open]`) restores full opacity. Kept in an inline
2275
- * <style> so the plugin stays zero-asset.
2369
+ * P3 focus mode (0.6.3: user-tuned "聚焦搭配"): when the eye toggle is armed,
2370
+ * an inline <style> (zero-asset, same pattern as the original FOCUS_CSS)
2371
+ * dims/collapses the harness content classes the USER opted into, at the
2372
+ * strength the user picked. Which content to dim and whether to additionally
2373
+ * collapse think is a persisted `prefs.focus` mix; the master on/off switch
2374
+ * stays the toolbar eye button.
2375
+ *
2376
+ * STABLE SELECTORS — researched against the rc.2 official build products:
2377
+ *
2378
+ * - think: `dsh-client-ui-conversation` renders every assistant reasoning
2379
+ * disclosure as a root `div[data-variant="think"][data-state="running|ok"]`
2380
+ * (ReasoningRow). The reasoning body lives INSIDE that root — the
2381
+ * DisclosureRow's children — so the root is a single clampable container:
2382
+ * CSS `max-height` + `overflow: hidden` collapses exactly the body while
2383
+ * the header row ("Think · summary") stays visible. → collapseThink is
2384
+ * feasible with pure CSS (no JS, no harness-internal interaction).
2385
+ *
2386
+ * - tool calls: `dsh-client-ui-tool` wraps EVERY atomic call (all ToolRow
2387
+ * presentation variants AND the bash-sample row) in
2388
+ * `div[data-chat-call-id]` (with `data-chat-anchor-key="call:<callId>"`);
2389
+ * no other card type in the harness uses that attribute (verified across
2390
+ * the whole @deepseek-ai install). → `[data-chat-call-id]` is the stable,
2391
+ * variant-independent tool-call-card selector.
2392
+ *
2393
+ * Hover/open restore mirrors the classic rule: `:hover` restores, and the
2394
+ * DisclosureRow inside the collapsed target sets `[data-open]` when the user
2395
+ * opens it (the DESCENDANT form `target [data-open]` is required — same as
2396
+ * pre-0.6.3). The collapse strip releases on hover AND on `[data-open]` so
2397
+ * an opened think disclosure never stays crushed.
2276
2398
  */
2277
- const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
2278
- [data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
2399
+ const FOCUS_SELECTOR_THINK = "[data-variant=\"think\"]";
2400
+ const FOCUS_SELECTOR_TOOL = "[data-chat-call-id]";
2401
+ /** Restored height when hovering/opening a collapsed think disclosure. */
2402
+ const FOCUS_THINK_MAX_HEIGHT = "78vh";
2279
2403
  /**
2280
- * Static settings-modal styling that needs `:hover` (which inline styles
2281
- * cannot express): the support-us card grid (micro-lift + accent highlight)
2282
- * and the pin-row hover wash. Accent values come from the rail root's CSS
2283
- * variables (`--ms-accent`), so one block serves every accent. The
2404
+ * Compose the focus-mode stylesheet from the persisted focus mix. Pure —
2405
+ * exported so unit tests can pin the exact rule text. `''` when no option is
2406
+ * armed (the master switch simply injects nothing).
2407
+ */
2408
+ function buildFocusCss(focus) {
2409
+ const { dimThink, dimTools, collapseThink } = focus;
2410
+ const strength = focus.opacity.toFixed(1);
2411
+ const rules = [];
2412
+ if (dimThink || collapseThink) {
2413
+ const decls = [];
2414
+ if (dimThink) decls.push(`opacity: ${strength}`);
2415
+ if (collapseThink) decls.push(`max-height: 36px`, "overflow: hidden");
2416
+ rules.push(`${FOCUS_SELECTOR_THINK} { ${decls.join("; ")}; transition: opacity 0.2s${collapseThink ? ", max-height 0.2s" : ""}; }`);
2417
+ rules.push(`${FOCUS_SELECTOR_THINK}:hover, ${FOCUS_SELECTOR_THINK} [data-open] { opacity: 1;${collapseThink ? ` max-height: ${FOCUS_THINK_MAX_HEIGHT};` : ""} }`);
2418
+ }
2419
+ if (dimTools) {
2420
+ rules.push(`${FOCUS_SELECTOR_TOOL} { opacity: ${strength}; transition: opacity 0.2s; }`);
2421
+ rules.push(`${FOCUS_SELECTOR_TOOL}:hover, ${FOCUS_SELECTOR_TOOL} [data-open] { opacity: 1; }`);
2422
+ }
2423
+ return rules.join("\n");
2424
+ }
2425
+ /**
2426
+ * Settings modal shared palette + geometry — one source for the inline
2427
+ * styles AND the static MODAL_CSS block below so they cannot drift. Dark
2428
+ * panel on a dark host, three text tiers (primary / section title / hint +
2429
+ * muted), one border tone, a 12px panel / 8px control radius scale and a
2430
+ * 4-unit spacing scale (4 / 8 / 12 / 16 / 20).
2431
+ */
2432
+ const MODAL_BG = "rgba(20, 24, 32, 0.98)";
2433
+ const MODAL_FG = "#e6e8ee";
2434
+ const MODAL_TITLE = "#c7cede";
2435
+ const MODAL_TEXT = "#b9c2d4";
2436
+ const MODAL_HINT = "#8b96ab";
2437
+ const MODAL_BORDER = "rgba(255, 255, 255, 0.14)";
2438
+ const MODAL_TIP_BG = "#222834";
2439
+ const MODAL_RADIUS_PANEL = 12;
2440
+ const MODAL_RADIUS_CONTROL = 8;
2441
+ /** Near-row description tip: how far its right edge sits from the row's right
2442
+ * edge — clears the 32px switch + its 10px padding. */
2443
+ const MODAL_TIP_RIGHT = 54;
2444
+ /**
2445
+ * Static settings-modal styling that needs `:hover`/`:focus-visible` (which
2446
+ * inline styles cannot express): the support-us card grid (micro-lift +
2447
+ * accent highlight), the pin-row / personal-header hover washes, one accent
2448
+ * focus ring for every modal control, the near-row tip's little arrow, the
2449
+ * personalization body's reveal, and a themed thin scrollbar. Accent values
2450
+ * come from the rail root's CSS variables (`--ms-accent*`), so one block
2451
+ * serves every accent. Inline styles keep these elements background-free
2452
+ * (except where noted) so the `:hover` washes below actually win. The
2284
2453
  * search-toggle recolor rule makes the EXTERNAL RailSearchUi chrome follow
2285
2454
  * the accent too (that file is owned by an earlier phase and cannot change);
2286
- * `!important` is required because the toggle's own inline styles win
2455
+ * `!important` is required because that toggle's own inline styles win
2287
2456
  * otherwise.
2288
2457
  */
2289
2458
  const MODAL_CSS = `
2290
2459
  [data-support-card] {
2291
2460
  display: flex; align-items: center; justify-content: center; gap: 8px;
2292
2461
  padding: 10px 12px; border-radius: 8px;
2293
- background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.12);
2462
+ background: rgba(255, 255, 255, 0.05); border: 1px solid ${MODAL_BORDER};
2294
2463
  color: #c7cede; text-decoration: none; font-size: 12.5px; line-height: 1.4;
2295
2464
  transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
2296
2465
  }
2297
2466
  [data-support-card]:hover { transform: translateY(-2px); border-color: var(--ms-accent); background: rgba(255, 255, 255, 0.09); }
2467
+ /* Row and header washes (the inline styles deliberately leave backgrounds
2468
+ unset so these rules win over the default padding-box background). */
2298
2469
  [data-toolbar-pin-toggle]:hover, [data-toolbar-pin-toggle]:focus-visible { background: rgba(255, 255, 255, 0.06); }
2470
+ [data-personal-toggle]:hover, [data-focus-toggle-settings]:hover { background: rgba(255, 255, 255, 0.05); }
2471
+ [data-focus-option]:hover { background: rgba(255, 255, 255, 0.04); }
2472
+ [data-toolbar-settings-close]:hover { background: rgba(255, 255, 255, 0.08); }
2473
+ [data-toolbar-settings-reset] { background: rgba(255, 255, 255, 0.06); }
2474
+ [data-toolbar-settings-reset]:hover { background: rgba(255, 255, 255, 0.1); }
2475
+ /* ONE accent ring for keyboard focus on every modal control. */
2476
+ [data-toolbar-pin-toggle]:focus-visible, [data-personal-toggle]:focus-visible,
2477
+ [data-focus-toggle-settings]:focus-visible,
2478
+ [data-toolbar-settings-close]:focus-visible, [data-toolbar-settings-reset]:focus-visible {
2479
+ box-shadow: 0 0 0 2px var(--ms-accent-soft);
2480
+ }
2481
+ /* Near-row description tip: a rotated square peeks out of the LEFT edge so
2482
+ the apex points back at the row's label. */
2483
+ [data-settings-tip]::before {
2484
+ content: ''; position: absolute; left: -3px; top: 50%;
2485
+ width: 7px; height: 7px; transform: translateY(-50%) rotate(45deg);
2486
+ background: ${MODAL_TIP_BG};
2487
+ border-left: 1px solid ${MODAL_BORDER};
2488
+ border-bottom: 1px solid ${MODAL_BORDER};
2489
+ }
2490
+ /* Tip + chevron motion lives here (not inline) so reduced-motion can kill it. */
2491
+ [data-settings-tip] { transition: opacity 140ms ease, transform 140ms ease, visibility 140ms; }
2492
+ [data-personal-toggle] svg, [data-focus-toggle-settings] svg { transition: transform 150ms ease; }
2493
+ /* One authored reveal: the personalization/focus bodies fade in on expand. */
2494
+ @keyframes ms-settings-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
2495
+ [data-settings-personal-body], [data-settings-focus-body] { animation: ms-settings-fade 140ms ease; }
2496
+ /* Thin themed scrollbar for the scrollable modal panel. */
2497
+ [data-toolbar-settings-panel]::-webkit-scrollbar { width: 10px; }
2498
+ [data-toolbar-settings-panel]::-webkit-scrollbar-thumb {
2499
+ background: rgba(255, 255, 255, 0.16); border-radius: 6px; border: 3px solid transparent; background-clip: padding-box;
2500
+ }
2501
+ [data-toolbar-settings-panel]::-webkit-scrollbar-track { background: transparent; }
2502
+ @media (prefers-reduced-motion: reduce) {
2503
+ [data-settings-tip], [data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-support-card] { transition: none; }
2504
+ [data-settings-personal-body], [data-settings-focus-body] { animation: none; }
2505
+ }
2299
2506
  [data-search-toggle] { color: #8b96ab !important; }
2300
2507
  [data-search-toggle][aria-pressed="true"] { background: var(--ms-accent-bg) !important; color: var(--ms-accent-soft) !important; }
2301
2508
  `;
2509
+ /**
2510
+ * Shared collapsible-section chrome (personalization + focus blocks): the
2511
+ * header button (chevron + title + live summary) and the summary text span.
2512
+ * One source so the two settings blocks cannot drift.
2513
+ */
2514
+ const SECTION_TOGGLE_STYLE = {
2515
+ display: "flex",
2516
+ alignItems: "center",
2517
+ gap: 8,
2518
+ width: "100%",
2519
+ padding: "8px 10px",
2520
+ border: "none",
2521
+ borderRadius: MODAL_RADIUS_CONTROL,
2522
+ cursor: "pointer",
2523
+ color: MODAL_TITLE,
2524
+ fontSize: 13,
2525
+ fontWeight: 600,
2526
+ textAlign: "left"
2527
+ };
2528
+ const SECTION_SUMMARY_STYLE = {
2529
+ flex: 1,
2530
+ minWidth: 0,
2531
+ textAlign: "right",
2532
+ fontWeight: 400,
2533
+ fontSize: 12,
2534
+ color: MODAL_HINT,
2535
+ overflow: "hidden",
2536
+ textOverflow: "ellipsis",
2537
+ whiteSpace: "nowrap"
2538
+ };
2302
2539
  /**
2303
2540
  * P3 deep links (`#msg=<anchor-key>`): initial delay before the first
2304
2541
  * deep-link attempt — the harness scrolls the conversation to the bottom on
@@ -2501,6 +2738,28 @@ window.__ModuleLoader__.load({
2501
2738
  return next;
2502
2739
  });
2503
2740
  };
2741
+ /** 0.6.3: patch ONE focus-mix flag/strength (nested field, same write-through). */
2742
+ const updateFocus = (patch) => {
2743
+ updatePrefs({ focus: {
2744
+ ...prefs.focus,
2745
+ ...patch
2746
+ } });
2747
+ };
2748
+ /**
2749
+ * 0.6.3: the focus block's live summary — the armed options joined into a
2750
+ * "聚焦搭配" line, plus the strength percentage. e.g. `think 淡化 · 强度 40%`.
2751
+ */
2752
+ const focusSummary = (() => {
2753
+ const parts = [
2754
+ prefs.focus.dimThink ? t("settings.focus.summary.think") : null,
2755
+ prefs.focus.dimTools ? t("settings.focus.summary.tools") : null,
2756
+ prefs.focus.collapseThink ? t("settings.focus.summary.collapse") : null
2757
+ ].filter((part) => part !== null);
2758
+ return t("settings.focus.summary", {
2759
+ opts: parts.length > 0 ? parts.join(" · ") : t("settings.focus.summary.none"),
2760
+ opacity: Math.round(prefs.focus.opacity * 100)
2761
+ });
2762
+ })();
2504
2763
  /** B1: flip one feature's pin — state and the persisted blob update together. */
2505
2764
  const onTogglePin = (id) => {
2506
2765
  setPrefs((prev) => {
@@ -2519,9 +2778,15 @@ window.__ModuleLoader__.load({
2519
2778
  const [expandHovered, setExpandHovered] = (0, react.useState)(false);
2520
2779
  const [settingsHovered, setSettingsHovered] = (0, react.useState)(false);
2521
2780
  const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
2522
- /** The feature whose description the settings modal's right pane shows
2523
- * (defaults to the first registry feature 'search'). */
2524
- const [descFeature, setDescFeature] = (0, react.useState)("search");
2781
+ /** The feature whose near-row description tip is currently visible
2782
+ * (`null` = none tips only appear on hover/focus of their own row). */
2783
+ const [descFeature, setDescFeature] = (0, react.useState)(null);
2784
+ /** B-design: the personalization block collapsess by default so only a
2785
+ * value summary leads the section; expanding reveals the controls. */
2786
+ const [personalOpen, setPersonalOpen] = (0, react.useState)(false);
2787
+ /** B-design (0.6.3): the focus block mirrors the personalization block —
2788
+ * collapsed by default, the header leads with a live option summary. */
2789
+ const [focusOpen, setFocusOpen] = (0, react.useState)(false);
2525
2790
  const settingsRef = (0, react.useRef)(null);
2526
2791
  const settingsBtnRef = (0, react.useRef)(null);
2527
2792
  const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
@@ -3130,27 +3395,25 @@ window.__ModuleLoader__.load({
3130
3395
  forkAt(mark.seq).then(() => setForkedKey(mark.key));
3131
3396
  };
3132
3397
  const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
3133
- const railStyle = {
3134
- position: "fixed",
3135
- top: railBox.top,
3136
- ...side === "left" ? { left: railBox.left } : { right: railBox.right },
3137
- height: railBox.height,
3138
- width: hit,
3139
- pointerEvents: "auto",
3140
- zIndex: 100,
3141
- display: "flex",
3142
- flexDirection: "column",
3143
- gap: 6,
3144
- paddingTop: 6,
3145
- "--ms-accent": accent,
3146
- "--ms-accent-soft": accentSoft,
3147
- "--ms-accent-bg": accentBg,
3148
- "--ms-icon": `${iconSize}px`,
3149
- "--ms-inset": `${inset}px`
3150
- };
3151
- const descKey = `settings.desc.${descFeature}`;
3152
3398
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3153
- style: railStyle,
3399
+ style: {
3400
+ position: "fixed",
3401
+ top: railBox.top,
3402
+ ...side === "left" ? { left: railBox.left } : { right: railBox.right },
3403
+ height: railBox.height,
3404
+ width: hit,
3405
+ pointerEvents: "auto",
3406
+ zIndex: 100,
3407
+ display: "flex",
3408
+ flexDirection: "column",
3409
+ gap: 6,
3410
+ paddingTop: 6,
3411
+ "--ms-accent": accent,
3412
+ "--ms-accent-soft": accentSoft,
3413
+ "--ms-accent-bg": accentBg,
3414
+ "--ms-icon": `${iconSize}px`,
3415
+ "--ms-inset": `${inset}px`
3416
+ },
3154
3417
  "aria-label": t("rail.label"),
3155
3418
  "data-focus-active": focusActive ? "true" : void 0,
3156
3419
  "data-accent": accent,
@@ -3159,7 +3422,7 @@ window.__ModuleLoader__.load({
3159
3422
  "data-inset": String(inset),
3160
3423
  children: [
3161
3424
  pulseCss !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: pulseCss }),
3162
- focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
3425
+ focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: buildFocusCss(prefs.focus) }),
3163
3426
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MODAL_CSS }),
3164
3427
  showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3165
3428
  type: "button",
@@ -3239,11 +3502,13 @@ window.__ModuleLoader__.load({
3239
3502
  width: "min(600px, 92vw)",
3240
3503
  maxHeight: "78vh",
3241
3504
  overflowY: "auto",
3242
- padding: "16px 18px",
3243
- background: "rgba(20, 24, 32, 0.98)",
3244
- color: "#e6e8ee",
3245
- borderRadius: 12,
3246
- boxShadow: "0 20px 60px rgba(0, 0, 0, 0.5)"
3505
+ padding: 20,
3506
+ background: MODAL_BG,
3507
+ color: MODAL_FG,
3508
+ borderRadius: MODAL_RADIUS_PANEL,
3509
+ boxShadow: "0 24px 64px rgba(0, 0, 0, 0.55)",
3510
+ scrollbarWidth: "thin",
3511
+ scrollbarColor: "rgba(255, 255, 255, 0.2) transparent"
3247
3512
  },
3248
3513
  children: [
3249
3514
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3252,14 +3517,14 @@ window.__ModuleLoader__.load({
3252
3517
  alignItems: "center",
3253
3518
  justifyContent: "space-between",
3254
3519
  gap: 8,
3255
- marginBottom: 12
3520
+ marginBottom: 18
3256
3521
  },
3257
3522
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3258
3523
  "data-toolbar-settings-title": true,
3259
3524
  style: {
3260
3525
  fontSize: 15,
3261
3526
  fontWeight: 600,
3262
- color: "#e6e8ee"
3527
+ color: MODAL_FG
3263
3528
  },
3264
3529
  children: t("settings.title")
3265
3530
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -3269,44 +3534,57 @@ window.__ModuleLoader__.load({
3269
3534
  title: t("settings.close"),
3270
3535
  onClick: closeSettings,
3271
3536
  style: {
3272
- width: 26,
3273
- height: 26,
3537
+ width: 28,
3538
+ height: 28,
3274
3539
  flexShrink: 0,
3275
3540
  display: "flex",
3276
3541
  alignItems: "center",
3277
3542
  justifyContent: "center",
3278
- background: "transparent",
3279
3543
  border: "none",
3280
3544
  padding: 0,
3281
3545
  cursor: "pointer",
3282
- color: "#8b96ab",
3283
- borderRadius: 6,
3284
- fontSize: 14,
3546
+ color: MODAL_HINT,
3547
+ borderRadius: MODAL_RADIUS_CONTROL,
3285
3548
  lineHeight: 1
3286
3549
  },
3287
- children: ""
3550
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3551
+ width: "14",
3552
+ height: "14",
3553
+ viewBox: "0 0 24 24",
3554
+ fill: "none",
3555
+ stroke: "currentColor",
3556
+ strokeWidth: "2",
3557
+ strokeLinecap: "round",
3558
+ "aria-hidden": "true",
3559
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })]
3560
+ })
3288
3561
  })]
3289
3562
  }),
3290
3563
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3291
3564
  "data-settings-section": true,
3292
- style: { marginBottom: 14 },
3293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3294
- "data-settings-section-title": true,
3295
- style: {
3296
- fontSize: 13,
3297
- fontWeight: 600,
3298
- color: "#c7cede",
3299
- marginBottom: 8
3300
- },
3301
- children: t("settings.section.features")
3302
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3303
- style: {
3304
- display: "grid",
3305
- gridTemplateColumns: "minmax(0, 1fr) 190px",
3306
- gap: 12,
3307
- alignItems: "stretch"
3308
- },
3309
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3565
+ style: { marginBottom: 20 },
3566
+ children: [
3567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3568
+ "data-settings-section-title": true,
3569
+ style: {
3570
+ fontSize: 13,
3571
+ fontWeight: 600,
3572
+ color: MODAL_TITLE,
3573
+ marginBottom: 4
3574
+ },
3575
+ children: t("settings.section.features")
3576
+ }),
3577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3578
+ "data-settings-pin-hint": true,
3579
+ style: {
3580
+ fontSize: 12,
3581
+ color: MODAL_HINT,
3582
+ lineHeight: 1.5,
3583
+ marginBottom: 10
3584
+ },
3585
+ children: t("settings.pin.hint")
3586
+ }),
3587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3310
3588
  style: {
3311
3589
  display: "flex",
3312
3590
  flexDirection: "column",
@@ -3314,92 +3592,163 @@ window.__ModuleLoader__.load({
3314
3592
  },
3315
3593
  children: toolbarFeatures.map((feature) => {
3316
3594
  const checked = pinned.includes(feature.id);
3317
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3318
- type: "button",
3319
- role: "switch",
3320
- "data-toolbar-pin-toggle": true,
3321
- "data-pin-id": feature.id,
3322
- "aria-checked": checked,
3323
- "aria-label": t(feature.labelKey),
3324
- onMouseEnter: () => setDescFeature(feature.id),
3325
- onFocus: () => setDescFeature(feature.id),
3326
- onClick: () => onTogglePin(feature.id),
3327
- style: {
3328
- display: "flex",
3329
- alignItems: "center",
3330
- justifyContent: "space-between",
3331
- gap: 10,
3332
- width: "100%",
3333
- padding: "7px 10px",
3334
- background: "transparent",
3335
- border: "none",
3336
- borderRadius: 8,
3337
- cursor: "pointer",
3338
- color: "#e6e8ee",
3339
- fontSize: 13,
3340
- textAlign: "left"
3341
- },
3342
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(feature.labelKey) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3343
- "aria-hidden": "true",
3595
+ const active = descFeature === feature.id;
3596
+ const tipId = `ms-settings-tip-${feature.id}`;
3597
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3598
+ "data-toolbar-pin-row": true,
3599
+ "data-row-id": feature.id,
3600
+ style: { position: "relative" },
3601
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3602
+ type: "button",
3603
+ role: "switch",
3604
+ "data-toolbar-pin-toggle": true,
3605
+ "data-pin-id": feature.id,
3606
+ "aria-checked": checked,
3607
+ "aria-label": t(feature.labelKey),
3608
+ "aria-describedby": tipId,
3609
+ onMouseEnter: () => setDescFeature(feature.id),
3610
+ onMouseLeave: () => setDescFeature((prev) => prev === feature.id ? null : prev),
3611
+ onFocus: () => setDescFeature(feature.id),
3612
+ onBlur: () => setDescFeature((prev) => prev === feature.id ? null : prev),
3613
+ onClick: () => onTogglePin(feature.id),
3344
3614
  style: {
3345
- position: "relative",
3346
- width: 32,
3347
- height: 18,
3348
- flexShrink: 0,
3349
- borderRadius: 9,
3350
- background: checked ? accentStrong : "rgba(255, 255, 255, 0.16)",
3351
- transition: "background 120ms ease"
3615
+ display: "flex",
3616
+ alignItems: "center",
3617
+ justifyContent: "space-between",
3618
+ gap: 10,
3619
+ width: "100%",
3620
+ padding: "8px 10px",
3621
+ border: "none",
3622
+ borderRadius: MODAL_RADIUS_CONTROL,
3623
+ cursor: "pointer",
3624
+ color: MODAL_FG,
3625
+ fontSize: 13,
3626
+ textAlign: "left"
3352
3627
  },
3353
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
3628
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3629
+ style: {
3630
+ overflow: "hidden",
3631
+ textOverflow: "ellipsis",
3632
+ whiteSpace: "nowrap"
3633
+ },
3634
+ children: t(feature.labelKey)
3635
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3636
+ "aria-hidden": "true",
3637
+ style: {
3638
+ position: "relative",
3639
+ width: 32,
3640
+ height: 18,
3641
+ flexShrink: 0,
3642
+ borderRadius: 9,
3643
+ background: checked ? accentStrong : "rgba(255, 255, 255, 0.16)",
3644
+ transition: "background 120ms ease"
3645
+ },
3646
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
3647
+ position: "absolute",
3648
+ top: 2,
3649
+ left: checked ? 16 : 2,
3650
+ width: 14,
3651
+ height: 14,
3652
+ borderRadius: "50%",
3653
+ background: "#ffffff",
3654
+ transition: "left 120ms ease"
3655
+ } })
3656
+ })]
3657
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3658
+ id: tipId,
3659
+ role: "tooltip",
3660
+ "data-settings-tip": true,
3661
+ "data-tip-for": feature.id,
3662
+ "data-tip-visible": active ? "true" : void 0,
3663
+ style: {
3354
3664
  position: "absolute",
3355
- top: 2,
3356
- left: checked ? 16 : 2,
3357
- width: 14,
3358
- height: 14,
3359
- borderRadius: "50%",
3360
- background: "#ffffff",
3361
- transition: "left 120ms ease"
3362
- } })
3665
+ right: MODAL_TIP_RIGHT,
3666
+ top: "50%",
3667
+ maxWidth: "55%",
3668
+ transform: `translateY(-50%) translateX(${active ? 0 : 4}px)`,
3669
+ padding: "5px 10px",
3670
+ borderRadius: MODAL_RADIUS_CONTROL,
3671
+ background: MODAL_TIP_BG,
3672
+ border: `1px solid ${MODAL_BORDER}`,
3673
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.35)",
3674
+ color: MODAL_TEXT,
3675
+ fontSize: 12,
3676
+ lineHeight: 1.45,
3677
+ opacity: active ? 1 : 0,
3678
+ visibility: active ? "visible" : "hidden",
3679
+ pointerEvents: "none",
3680
+ zIndex: 4
3681
+ },
3682
+ children: t(`settings.desc.${feature.id}`)
3363
3683
  })]
3364
3684
  }, feature.id);
3365
3685
  })
3366
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3367
- "data-settings-desc": true,
3368
- "aria-live": "polite",
3369
- style: {
3370
- display: "flex",
3371
- alignItems: "center",
3372
- padding: "10px 12px",
3373
- borderRadius: 8,
3374
- background: "rgba(255, 255, 255, 0.05)",
3375
- color: "#b9c2d4",
3376
- fontSize: 12.5,
3377
- lineHeight: 1.5
3378
- },
3379
- children: t(descKey)
3380
- })]
3381
- })]
3686
+ })
3687
+ ]
3382
3688
  }),
3383
3689
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3384
3690
  "data-settings-section": true,
3385
3691
  "data-settings-personal": true,
3386
- style: { marginBottom: 14 },
3387
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3388
- "data-settings-section-title": true,
3389
- style: {
3390
- fontSize: 13,
3391
- fontWeight: 600,
3392
- color: "#c7cede",
3393
- marginBottom: 8
3394
- },
3395
- children: t("settings.section.personal")
3396
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3692
+ style: { marginBottom: 20 },
3693
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3694
+ type: "button",
3695
+ "data-personal-toggle": true,
3696
+ "aria-expanded": personalOpen,
3697
+ "aria-label": personalOpen ? t("settings.personal.collapse") : t("settings.personal.expand"),
3698
+ title: personalOpen ? t("settings.personal.collapse") : t("settings.personal.expand"),
3699
+ onClick: () => setPersonalOpen((v) => !v),
3700
+ style: SECTION_TOGGLE_STYLE,
3701
+ children: [
3702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3703
+ width: "14",
3704
+ height: "14",
3705
+ viewBox: "0 0 24 24",
3706
+ fill: "none",
3707
+ stroke: "currentColor",
3708
+ strokeWidth: "2.5",
3709
+ strokeLinecap: "round",
3710
+ strokeLinejoin: "round",
3711
+ "aria-hidden": "true",
3712
+ style: {
3713
+ flexShrink: 0,
3714
+ transform: personalOpen ? "rotate(90deg)" : "none"
3715
+ },
3716
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
3717
+ }),
3718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3719
+ "data-settings-section-title": true,
3720
+ style: { flexShrink: 0 },
3721
+ children: t("settings.section.personal")
3722
+ }),
3723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3724
+ "data-settings-personal-summary": true,
3725
+ style: SECTION_SUMMARY_STYLE,
3726
+ children: t("settings.personal.summary", {
3727
+ accent,
3728
+ icon: iconSize,
3729
+ side: side === "left" ? t("settings.side.left") : t("settings.side.right")
3730
+ })
3731
+ })
3732
+ ]
3733
+ }), personalOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3734
+ "data-settings-personal-body": true,
3397
3735
  style: {
3736
+ padding: "10px 4px 8px",
3398
3737
  display: "flex",
3399
3738
  flexDirection: "column",
3400
- gap: 10
3739
+ gap: 12
3401
3740
  },
3402
3741
  children: [
3742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3743
+ "data-settings-personal-hint": true,
3744
+ style: {
3745
+ fontSize: 12,
3746
+ color: MODAL_HINT,
3747
+ lineHeight: 1.5,
3748
+ padding: "0 6px"
3749
+ },
3750
+ children: t("settings.personal.hint")
3751
+ }),
3403
3752
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3404
3753
  style: {
3405
3754
  display: "flex",
@@ -3410,7 +3759,7 @@ window.__ModuleLoader__.load({
3410
3759
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3411
3760
  style: {
3412
3761
  fontSize: 12.5,
3413
- color: "#8b96ab",
3762
+ color: MODAL_HINT,
3414
3763
  width: 90,
3415
3764
  flexShrink: 0
3416
3765
  },
@@ -3448,7 +3797,7 @@ window.__ModuleLoader__.load({
3448
3797
  alignItems: "center",
3449
3798
  gap: 6,
3450
3799
  fontSize: 12.5,
3451
- color: "#b9c2d4",
3800
+ color: MODAL_TEXT,
3452
3801
  cursor: "pointer"
3453
3802
  },
3454
3803
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -3479,7 +3828,7 @@ window.__ModuleLoader__.load({
3479
3828
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3480
3829
  style: {
3481
3830
  fontSize: 12.5,
3482
- color: "#8b96ab",
3831
+ color: MODAL_HINT,
3483
3832
  width: 90,
3484
3833
  flexShrink: 0
3485
3834
  },
@@ -3503,7 +3852,7 @@ window.__ModuleLoader__.load({
3503
3852
  "data-icon-size-value": true,
3504
3853
  style: {
3505
3854
  fontSize: 12.5,
3506
- color: "#b9c2d4",
3855
+ color: MODAL_TEXT,
3507
3856
  width: 40
3508
3857
  },
3509
3858
  children: [iconSize, "px"]
@@ -3521,7 +3870,7 @@ window.__ModuleLoader__.load({
3521
3870
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3522
3871
  style: {
3523
3872
  fontSize: 12.5,
3524
- color: "#8b96ab",
3873
+ color: MODAL_HINT,
3525
3874
  width: 90,
3526
3875
  flexShrink: 0
3527
3876
  },
@@ -3545,7 +3894,7 @@ window.__ModuleLoader__.load({
3545
3894
  "data-inset-value": true,
3546
3895
  style: {
3547
3896
  fontSize: 12.5,
3548
- color: "#b9c2d4",
3897
+ color: MODAL_TEXT,
3549
3898
  width: 40
3550
3899
  },
3551
3900
  children: [inset, "px"]
@@ -3565,7 +3914,7 @@ window.__ModuleLoader__.load({
3565
3914
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3566
3915
  style: {
3567
3916
  fontSize: 12.5,
3568
- color: "#8b96ab",
3917
+ color: MODAL_HINT,
3569
3918
  width: 90,
3570
3919
  flexShrink: 0
3571
3920
  },
@@ -3577,7 +3926,7 @@ window.__ModuleLoader__.load({
3577
3926
  alignItems: "center",
3578
3927
  gap: 5,
3579
3928
  fontSize: 13,
3580
- color: "#e6e8ee",
3929
+ color: MODAL_FG,
3581
3930
  cursor: "pointer"
3582
3931
  },
3583
3932
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -3595,7 +3944,7 @@ window.__ModuleLoader__.load({
3595
3944
  alignItems: "center",
3596
3945
  gap: 5,
3597
3946
  fontSize: 13,
3598
- color: "#e6e8ee",
3947
+ color: MODAL_FG,
3599
3948
  cursor: "pointer"
3600
3949
  },
3601
3950
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -3612,16 +3961,177 @@ window.__ModuleLoader__.load({
3612
3961
  ]
3613
3962
  })]
3614
3963
  }),
3964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3965
+ "data-settings-section": true,
3966
+ "data-focus-settings": true,
3967
+ style: { marginBottom: 20 },
3968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3969
+ type: "button",
3970
+ "data-focus-toggle-settings": true,
3971
+ "aria-expanded": focusOpen,
3972
+ "aria-label": focusOpen ? t("settings.focus.collapse") : t("settings.focus.expand"),
3973
+ title: focusOpen ? t("settings.focus.collapse") : t("settings.focus.expand"),
3974
+ onClick: () => setFocusOpen((v) => !v),
3975
+ style: SECTION_TOGGLE_STYLE,
3976
+ children: [
3977
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3978
+ width: "14",
3979
+ height: "14",
3980
+ viewBox: "0 0 24 24",
3981
+ fill: "none",
3982
+ stroke: "currentColor",
3983
+ strokeWidth: "2.5",
3984
+ strokeLinecap: "round",
3985
+ strokeLinejoin: "round",
3986
+ "aria-hidden": "true",
3987
+ style: {
3988
+ flexShrink: 0,
3989
+ transform: focusOpen ? "rotate(90deg)" : "none"
3990
+ },
3991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
3992
+ }),
3993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3994
+ "data-settings-section-title": true,
3995
+ style: { flexShrink: 0 },
3996
+ children: t("settings.section.focus")
3997
+ }),
3998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3999
+ "data-focus-summary": true,
4000
+ style: SECTION_SUMMARY_STYLE,
4001
+ children: focusSummary
4002
+ })
4003
+ ]
4004
+ }), focusOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4005
+ "data-settings-focus-body": true,
4006
+ style: {
4007
+ padding: "10px 4px 8px",
4008
+ display: "flex",
4009
+ flexDirection: "column",
4010
+ gap: 12
4011
+ },
4012
+ children: [
4013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4014
+ "data-settings-focus-hint": true,
4015
+ style: {
4016
+ fontSize: 12,
4017
+ color: MODAL_HINT,
4018
+ lineHeight: 1.5,
4019
+ padding: "0 6px"
4020
+ },
4021
+ children: t("settings.focus.hint")
4022
+ }),
4023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4024
+ "data-focus-option": true,
4025
+ style: {
4026
+ display: "flex",
4027
+ alignItems: "center",
4028
+ gap: 8,
4029
+ padding: "6px 8px",
4030
+ borderRadius: MODAL_RADIUS_CONTROL,
4031
+ fontSize: 13,
4032
+ color: MODAL_FG,
4033
+ cursor: "pointer"
4034
+ },
4035
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4036
+ type: "checkbox",
4037
+ "data-focus-dim-think": true,
4038
+ checked: prefs.focus.dimThink,
4039
+ onChange: (e) => updateFocus({ dimThink: e.target.checked })
4040
+ }), t("settings.focus.dimThink")]
4041
+ }),
4042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4043
+ "data-focus-option": true,
4044
+ style: {
4045
+ display: "flex",
4046
+ alignItems: "center",
4047
+ gap: 8,
4048
+ padding: "6px 8px",
4049
+ borderRadius: MODAL_RADIUS_CONTROL,
4050
+ fontSize: 13,
4051
+ color: MODAL_FG,
4052
+ cursor: "pointer"
4053
+ },
4054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4055
+ type: "checkbox",
4056
+ "data-focus-dim-tools": true,
4057
+ checked: prefs.focus.dimTools,
4058
+ onChange: (e) => updateFocus({ dimTools: e.target.checked })
4059
+ }), t("settings.focus.dimTools")]
4060
+ }),
4061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4062
+ "data-focus-option": true,
4063
+ style: {
4064
+ display: "flex",
4065
+ alignItems: "center",
4066
+ gap: 8,
4067
+ padding: "6px 8px",
4068
+ borderRadius: MODAL_RADIUS_CONTROL,
4069
+ fontSize: 13,
4070
+ color: MODAL_FG,
4071
+ cursor: "pointer"
4072
+ },
4073
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4074
+ type: "checkbox",
4075
+ "data-focus-collapse-think": true,
4076
+ checked: prefs.focus.collapseThink,
4077
+ onChange: (e) => updateFocus({ collapseThink: e.target.checked })
4078
+ }), t("settings.focus.collapseThink")]
4079
+ }),
4080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4081
+ style: {
4082
+ display: "flex",
4083
+ alignItems: "center",
4084
+ gap: 10,
4085
+ flexWrap: "wrap"
4086
+ },
4087
+ children: [
4088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4089
+ style: {
4090
+ fontSize: 12.5,
4091
+ color: MODAL_HINT,
4092
+ width: 90,
4093
+ flexShrink: 0
4094
+ },
4095
+ children: t("settings.focus.opacity")
4096
+ }),
4097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4098
+ type: "range",
4099
+ "data-focus-opacity": true,
4100
+ min: .2,
4101
+ max: .8,
4102
+ step: .1,
4103
+ value: prefs.focus.opacity,
4104
+ onChange: (e) => updateFocus({ opacity: Number(e.target.value) }),
4105
+ style: {
4106
+ flex: 1,
4107
+ minWidth: 140,
4108
+ maxWidth: 260
4109
+ }
4110
+ }),
4111
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4112
+ "data-focus-opacity-value": true,
4113
+ style: {
4114
+ fontSize: 12.5,
4115
+ color: MODAL_TEXT,
4116
+ width: 44
4117
+ },
4118
+ children: [Math.round(prefs.focus.opacity * 100), "%"]
4119
+ })
4120
+ ]
4121
+ })
4122
+ ]
4123
+ })]
4124
+ }),
3615
4125
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3616
4126
  "data-settings-section": true,
3617
4127
  "data-settings-lang": true,
3618
- style: { marginBottom: 14 },
4128
+ style: { marginBottom: 20 },
3619
4129
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3620
4130
  "data-settings-section-title": true,
3621
4131
  style: {
3622
4132
  fontSize: 13,
3623
4133
  fontWeight: 600,
3624
- color: "#c7cede",
4134
+ color: MODAL_TITLE,
3625
4135
  marginBottom: 8
3626
4136
  },
3627
4137
  children: t("settings.language")
@@ -3644,7 +4154,7 @@ window.__ModuleLoader__.load({
3644
4154
  alignItems: "center",
3645
4155
  gap: 5,
3646
4156
  fontSize: 13,
3647
- color: "#e6e8ee",
4157
+ color: MODAL_FG,
3648
4158
  cursor: "pointer"
3649
4159
  },
3650
4160
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -3664,7 +4174,7 @@ window.__ModuleLoader__.load({
3664
4174
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3665
4175
  style: {
3666
4176
  fontSize: 12,
3667
- color: "#8b96ab",
4177
+ color: MODAL_HINT,
3668
4178
  marginBottom: 10
3669
4179
  },
3670
4180
  children: t("settings.support")
@@ -3771,13 +4281,12 @@ window.__ModuleLoader__.load({
3771
4281
  onClick: onResetAll,
3772
4282
  style: {
3773
4283
  display: "block",
3774
- margin: "14px auto 0",
4284
+ margin: "16px auto 2px",
3775
4285
  padding: "7px 16px",
3776
- background: "rgba(255, 255, 255, 0.06)",
3777
- border: "1px solid rgba(255, 255, 255, 0.14)",
3778
- borderRadius: 8,
4286
+ border: `1px solid ${MODAL_BORDER}`,
4287
+ borderRadius: MODAL_RADIUS_CONTROL,
3779
4288
  cursor: "pointer",
3780
- color: "#b9c2d4",
4289
+ color: MODAL_TEXT,
3781
4290
  fontSize: 12.5
3782
4291
  },
3783
4292
  children: t("settings.reset")
@@ -3821,7 +4330,7 @@ window.__ModuleLoader__.load({
3821
4330
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3822
4331
  style: { color: "#8b96ab" },
3823
4332
  children: [t("update.current"), ": "]
3824
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.2" })] }),
4333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.3" })] }),
3825
4334
  updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3826
4335
  "data-update-latest": true,
3827
4336
  children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-milestone",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Git-style milestone timeline for DeepSeek Harness: hover for metadata, click to jump to any message. 会话里程碑导航条:圆点时间线,定位并跳转到每条提问。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",