dsh-zen-remote 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  <p align="center">
5
5
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT"></a>
6
- <img src="https://img.shields.io/badge/release-v1.1.4-5B4CF0?style=flat-square" alt="v1.1.4">
6
+ <img src="https://img.shields.io/badge/release-v1.1.6-5B4CF0?style=flat-square" alt="v1.1.6">
7
7
  <img src="https://img.shields.io/badge/DSH-Web%20Profile-5B4CF0?style=flat-square" alt="DSH Web Profile">
8
8
  </p>
9
9
 
@@ -39,7 +39,7 @@ dsh plugin add dsh-zen-remote
39
39
  ```jsonc
40
40
  {
41
41
  "dependencies": {
42
- "dsh-zen-remote": "^1.1.4" // 本地开发换成 "link:/path/to/dsh-zen-remote"
42
+ "dsh-zen-remote": "^1.1.6" // 本地开发换成 "link:/path/to/dsh-zen-remote"
43
43
  },
44
44
  "dsh": { "profile": { "bundles": [
45
45
  "@deepseek-ai/dsh-base",
package/docs/interface.md CHANGED
@@ -115,6 +115,21 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
115
115
  > `v2.0.0` 及更早的小节标题用的是手机端界面的代次,与 npm 包版本号不是一条线;
116
116
  > 两套编号并行容易看混,从 `1.1.0` 起本表统一按 **npm 包版本**记录。
117
117
 
118
+ ### 未发布
119
+
120
+ **修复**
121
+
122
+ - 适配 DSH `0.1.1`:活动 chip 点击失效(点了退回信息卡)。三处上游变化同时命中
123
+ 旧方案——子代理入口从 `header.actions` 槽移进新的 `header.lineage` 槽(面包屑
124
+ 内);其根节点 class 变成 `"ZKlsPq_root "`(**带尾随空格**,`[class$="_root"]`
125
+ 整串后缀匹配直接失配);触发器**只认可信输入**,`.click()` 与合成
126
+ pointer/mouse 事件一律无效(真机逐项排除了插件样式的嫌疑后实测)。因此废弃
127
+ 「转发点击」,改为 `effects/native-trigger-overlay.ts` 把**真实的官方触发器**
128
+ 透明地铺在 pill 上——用户手指点到的就是官方元素,可信输入天然成立;弹层在
129
+ 0.1.1 里 portal 到 body 自行定位(336px,屏内),旧的锚定/重锚 CSS 一并删除。
130
+ 选择器统一改为按 header 定位、按 ARIA(`aria-haspopup="tree"`)区分,不再依赖
131
+ 槽名与根节点类名。
132
+
118
133
  ### 1.1.0
119
134
 
120
135
  **新增**
package/lib/client.js CHANGED
@@ -1094,30 +1094,42 @@ function IconTaskOutline14({ size = 14 }) {
1094
1094
  return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "8", r: "6.1", stroke: "currentColor", strokeWidth: "1.3" }), (0, jsx_runtime_1.jsx)("path", { d: "M8 4.4V8l2.5 1.7", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" })] }));
1095
1095
  }
1096
1096
  /**
1097
- * The official triggers our pills stand in for. Both are hidden and parked
1098
- * as invisible anchors at the right end of the view-switch band
1099
- * (styles/header.css.ts) so their own popovers still mount and position from
1097
+ * The official triggers our pills stand in for. Both are hidden and parked as
1098
+ * invisible anchors at the right end of the view-switch band
1099
+ * (styles/header.css.ts) so their popovers still mount and position from
1100
1100
  * there; the pill is the visible control and forwards the tap.
1101
1101
  *
1102
- * Told apart by `aria-haspopup`, not by class: the subagent entry declares
1103
- * `tree` (its menu is a session tree) while the jobs entry declares none.
1104
- * Both roots/triggers only expose build-hashed class names, so the ARIA
1105
- * contract is the one stable discriminator between two otherwise identical
1106
- * `[class$="_root"] > button[class$="_trigger"]` shapes.
1102
+ * Told apart by `aria-haspopup`: the subagent entry declares `tree` (its menu
1103
+ * is a session tree), the jobs entry declares none.
1104
+ *
1105
+ * Scoped to the HEADER, not to a slot (2026-08-21). DSH 0.1.1 moved the
1106
+ * subagent entry out of `conversation.session.header.actions` into a new
1107
+ * `conversation.session.header.lineage` slot inside the breadcrumb; every
1108
+ * slot-scoped lookup here silently stopped matching and the pill fell back to
1109
+ * opening the info card. The header is the stable boundary — which slot
1110
+ * inside it owns the entry is the suite's business, and it has changed once.
1111
+ *
1112
+ * Also deliberately NOT keyed on the root's class. In 0.1.1 that attribute is
1113
+ * `"ZKlsPq_root "` — with a TRAILING SPACE — so `[class$="_root"]` does not
1114
+ * match it at all (`[class$=]` tests the whole attribute string). The
1115
+ * trigger's own class has no such tail, so anchor on the trigger and reach
1116
+ * the root through `:has()` instead of naming it.
1107
1117
  */
1108
- const ACTIONS_SLOT = '[data-slot="conversation.session.header.actions"]';
1109
- const SUBAGENT_TRIGGER = `${ACTIONS_SLOT} button[aria-haspopup="tree"]`;
1110
- const JOBS_TRIGGER = `${ACTIONS_SLOT} > [class$="_root"] > button[class$="_trigger"]:not([aria-haspopup])`;
1118
+ const HEADER = '[data-phase] header';
1119
+ const SUBAGENT_TRIGGER = `${HEADER} button[aria-haspopup="tree"]`;
1120
+ const JOBS_TRIGGER = `${HEADER} button[class$="_trigger"]:not([aria-haspopup])`;
1111
1121
  /** One glanceable group: icon + count + state dot; opens the official popover. */
1112
1122
  function ActivityPill({ kind, count, state, label, trigger, onFallback, Icon }) {
1113
1123
  if (count === 0)
1114
1124
  return null;
1115
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "activity-pill", "data-activity-kind": kind, "data-activity-state": state, "aria-label": label, title: label, onClick: () => {
1116
- const el = document.querySelector(trigger);
1117
- if (el === null)
1125
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "activity-pill", "data-activity-kind": kind, "data-activity-state": state, "aria-label": label, title: label,
1126
+ /* Reached only when the official trigger is absent — when it exists,
1127
+ effects/native-trigger-overlay.ts lays it transparently over this
1128
+ pill and the tap never gets here. Scripting the official click is
1129
+ not an option: 0.1.1 ignores synthetic events (see that file). */
1130
+ onClick: () => {
1131
+ if (document.querySelector(trigger) === null)
1118
1132
  onFallback();
1119
- else
1120
- el.click();
1121
1133
  }, children: [(0, jsx_runtime_1.jsx)(Icon, {}), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "activity-count", children: count }), (0, jsx_runtime_1.jsx)("i", { "data-mobile-nav": "activity-dot", "aria-hidden": "true" })] }));
1122
1134
  }
1123
1135
  /**
@@ -3975,6 +3987,13 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
3975
3987
  widths, only the center 1fr column shrinks by 16px. */
3976
3988
  [data-phase] header {
3977
3989
  position: relative;
3990
+ /* Above the scroller's plain and relative-positioned message content, so
3991
+ the ::after fade strip below actually paints over the messages sliding
3992
+ under it (positioned z-auto boxes otherwise win by DOM order — the
3993
+ scroller comes after the header). Deliberately tiny: the seat (7),
3994
+ scroll-to-bottom (8) and every overlay this plugin owns (30+) stay
3995
+ above. */
3996
+ z-index: 2;
3978
3997
  padding: 0 8px 28px !important;
3979
3998
  }
3980
3999
  /* No header bottom line — fade instead (real-device round 2, 2026-08-17).
@@ -3982,14 +4001,20 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
3982
4001
  \`border-bottom\` on the header itself is transparent, just a layout
3983
4002
  reserve — dsh-client-ui-conversation lib/client.js ".wSkVaW_header{
3984
4003
  border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;
3985
- position:absolute;bottom:1px...}", verified live 2026-08-17), so
3986
- hiding the pseudo-element is enough; the header's own border-bottom
3987
- never had a visible color to begin with. The message scroller now
3988
- fades in from under the header instead (styles/composer.css.ts, "no
3989
- divider above OR below the message list" the two edges share one
3990
- mask-image on [class$="_scrollBody"]). */
4004
+ position:absolute;bottom:1px...}", verified live 2026-08-17), so the
4005
+ bar is repurposed rather than hidden: restyled into a 20px strip
4006
+ hanging just below the header that fades the messages in the same
4007
+ look the scroller's mask-image used to produce, without a mask on a
4008
+ scroll container (iOS WebKit renders that as fog over the header, see
4009
+ composer.css.ts "no divider above OR below the message list",
4010
+ 2026-08-23). color-mix instead of the transparent keyword: the host
4011
+ writes its own seat gradient that way, and plain \`transparent\` is
4012
+ rgba(0,0,0,0) — WebKit interpolates it through grey. */
3991
4013
  [data-phase] header::after {
3992
- display: none !important;
4014
+ height: 20px !important;
4015
+ top: 100% !important;
4016
+ bottom: auto !important;
4017
+ background: linear-gradient(to bottom, var(--dsw-alias-bg-base), color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent)) !important;
3993
4018
  }
3994
4019
  /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for
3995
4020
  the "标题被挤下去" report). The three items are placed with explicit
@@ -4177,48 +4202,40 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
4177
4202
  [data-phase] header [data-slot="conversation.session.header.utilities"] > * {
4178
4203
  display: none !important;
4179
4204
  }
4180
- /* Native background-task / subagent entries: kept mounted, but parked as
4181
- ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.
4182
- Our activity pill is the visible control and forwards the tap to the
4183
- official trigger, so the official popover — a child of this root — still
4184
- mounts and positions from here.
4185
-
4186
- Why not just leave them visible in the header: each is ~103px wide while
4187
- headerActions' grid column is 92px, and flex children that cannot shrink
4188
- simply overflow measured at 375px the subagent entry ran x=60..163
4189
- straight across the centred title at x=100..275.
4190
-
4191
- Why not display:none them either: the menu lives inside the root, so
4192
- hiding the root takes the popover with it and the pill would have
4193
- nothing to open. Hide the TRIGGER (below) and keep the root as a box. */
4194
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"]:has(> button[class$="_trigger"]) {
4195
- display: block !important;
4196
- position: absolute !important;
4197
- right: 0 !important;
4198
- top: 100% !important;
4199
- width: 0 !important;
4200
- height: 28px !important;
4201
- overflow: visible !important;
4202
- order: 0 !important;
4205
+ /* Native background-task / subagent entries (rewritten for DSH 0.1.1,
4206
+ 2026-08-21). Three upstream changes killed the old "park the root as a
4207
+ zero-width anchor, hide the trigger, forward the tap" design at once:
4208
+
4209
+ - the subagent entry moved out of header.actions into the new
4210
+ conversation.session.header.lineage slot (inside the breadcrumb);
4211
+ - its root's class attribute is now "ZKlsPq_root " with a TRAILING
4212
+ SPACE so a [class$="_root"] anchor matches nothing at all;
4213
+ - the trigger only reacts to TRUSTED input: .click() and synthetic
4214
+ pointer/mouse events leave aria-expanded untouched (measured on device
4215
+ with every plugin override stripped), so forwarding a tap in JS can
4216
+ never open it again.
4217
+
4218
+ The trigger's geometry is therefore owned at runtime by
4219
+ effects/native-trigger-overlay.ts, which lays the REAL button
4220
+ transparently over the activity pill — a genuine finger tap lands on the
4221
+ official element. Nothing in this stylesheet may size, hide, or set
4222
+ pointer-events on the trigger: that is exactly what would take the tap
4223
+ target away again.
4224
+
4225
+ CSS keeps only two chores. Dissolve the root so its box (and its
4226
+ stray separator, hidden below) stops occupying the breadcrumb, and let
4227
+ the trigger participate from wherever the overlay pins it. And no menu
4228
+ re-anchoring anymore: the 0.1.1 popover portals to <body> as
4229
+ position:fixed and positions itself from the trigger's viewport rect
4230
+ (measured 336px wide, fully on screen at 411px). */
4231
+ [data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
4232
+ [data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
4233
+ display: contents !important;
4203
4234
  }
4204
- /* The official trigger never shows — the pill replaced it. Programmatic
4205
- .click() still reaches React's handler on a display:none button. */
4206
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"] > button[class$="_trigger"] {
4235
+ [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_separator"],
4236
+ [data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
4207
4237
  display: none !important;
4208
4238
  }
4209
- /* Anchor the popover to the RIGHT edge and clamp it to the viewport. The
4210
- root now sits at the right edge with zero width, so the official
4211
- \`left: 0\` would start the 336px panel off-screen; layout.css.ts's
4212
- \`left: 8px\` re-anchor was written for a viewport-width containing block
4213
- that this element is not. Right-anchoring is the one that holds for both
4214
- (2026-08-20: the old pairing put the panel 29px past the right edge). */
4215
- [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_menu"] {
4216
- left: auto !important;
4217
- right: 0 !important;
4218
- width: min(336px, calc(100vw - 32px)) !important;
4219
- max-width: none !important;
4220
- max-height: min(420px, calc(100dvh - 140px)) !important;
4221
- }
4222
4239
  /* The agent-preset mode badge specifically survives the blanket hide
4223
4240
  above: layout.css.ts's shared <=1023px block targets it directly
4224
4241
  (header [class$="_label"]:has(> svg) { display: block !important }),
@@ -4343,7 +4360,11 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
4343
4360
  touch-action: manipulation;
4344
4361
  -webkit-tap-highlight-color: transparent;
4345
4362
  }
4363
+ /* pointer-events:none so the invisible official trigger laid over this pill
4364
+ (effects/native-trigger-overlay.ts) is what a tap reaches. The pill is
4365
+ visuals only. */
4346
4366
  [data-mobile-nav="activity-pill"] {
4367
+ pointer-events: none;
4347
4368
  display: inline-flex;
4348
4369
  align-items: center;
4349
4370
  gap: 3px;
@@ -4665,22 +4686,29 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4665
4686
  }
4666
4687
 
4667
4688
  /* --- 6. no divider above OR below the message list ---
4668
- Instead of a rule the messages butt against, the message scroller
4669
- fades out over its last 26px (S3, no divider above the composer) AND
4670
- fades in over its first 20px (S3.1 real-device round 2: the header's
4671
- own bottom line is removed too, styles/header.css.ts's
4672
- \`header::after\` rule). The mask lives on the scroll body (NOT on the
4673
- header or the composer): a mask clips everything it paints, and both
4674
- surfaces host position:fixed children (composer's permission/model
4675
- sheets, any future header overlay) that must not be clipped.
4676
- \`mask-image\` can only be declared once per element, so both fades are
4677
- ONE linear-gradient rather than two separate declarations (the second
4678
- would silently replace the first) this is the merge of what used to
4679
- be S3's bottom-only mask. */
4680
- [class$="_scrollBody"] {
4681
- -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
4682
- mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
4683
- }
4689
+ Instead of a rule the messages butt against, they fade near both edges.
4690
+ This used to be ONE mask-image on the message scroller; iOS WebKit
4691
+ turned that into fog over the header (user report 2026-08-23): a mask
4692
+ on a scroll container is applied in CONTENT coordinates there (the fade
4693
+ bands scroll away with the messages) and, worse, it breaks the
4694
+ container's own overflow clipping content scrolled out of view keeps
4695
+ painting, straight over the header and status bar. Engine bug with no
4696
+ iOS escape hatch (every iOS browser is WebKit), so the fades moved off
4697
+ the scroller entirely:
4698
+
4699
+ - fade-IN under the header: the header's own ::after, restyled into a
4700
+ 20px gradient strip — styles/header.css.ts ("no header bottom line").
4701
+ - fade-OUT above the composer: nothing of ours. The official composer
4702
+ seat already carries \`background: linear-gradient(transparent 0,
4703
+ bg 36px)\` on its sticky self (dsh-client-ui-conversation lib/
4704
+ client.js .wSkVaW_composerSeat) and that is the entire visible
4705
+ effect; the mask's own bottom band landed inside the safe-area
4706
+ padding below the card and painted nothing. If the bottom edge ever
4707
+ reads harsh, hang a ::before strip off the seat the same way the
4708
+ header does — do NOT put a mask back on the scroller.
4709
+
4710
+ Painted strips instead of a mask is a fair trade only because the page
4711
+ background is one flat color — which it is (--dsw-alias-bg-base). */
4684
4712
  [class$="_composerSeat"],
4685
4713
  [class$="_composerStack"] {
4686
4714
  border-top: none !important;
@@ -7483,6 +7511,108 @@ function installModalBack(ctx) {
7483
7511
  }, 'dsh-zen-remote: modal back gesture');
7484
7512
  }
7485
7513
  };
7514
+ __modules["effects/native-trigger-overlay.js"] = function (require, module, exports) {
7515
+ "use strict";
7516
+ /**
7517
+ * Lay each official header trigger transparently over the activity pill that
7518
+ * stands in for it, so a real finger tap lands on the official element.
7519
+ *
7520
+ * Why not just forward the tap (what this replaced): DSH 0.1.1's subagent
7521
+ * trigger only reacts to TRUSTED input. Measured on device 2026-08-21 —
7522
+ * `.click()`, and synthetic `pointerdown`/`mousedown`/`pointerup`, all leave
7523
+ * `aria-expanded` at "false"; a CDP-dispatched real touch at the same point
7524
+ * opens it. That was verified with every plugin style override stripped off
7525
+ * the trigger, so it is the event path, not our CSS. Any design that scripts
7526
+ * the click is therefore dead, and this one moves the real control instead.
7527
+ *
7528
+ * Why this is now cheap: in 0.1.1 the popover portals to <body> as a
7529
+ * `position: fixed` layer (`ZKlsPq_menu`, z-index 100) and positions itself
7530
+ * from the trigger's viewport rect — measured 336px wide, fully on screen.
7531
+ * So placing the trigger over the pill also lands the popover at the pill,
7532
+ * and none of the old "park an anchor, re-anchor the menu" CSS is needed.
7533
+ *
7534
+ * The pill keeps the visuals (icon, count, state dot); the invisible official
7535
+ * trigger on top keeps the behaviour. Pills are `pointer-events: none` so a
7536
+ * tap can only ever reach the real thing — no double handling.
7537
+ */
7538
+ Object.defineProperty(exports, "__esModule", { value: true });
7539
+ exports.installNativeTriggerOverlay = installNativeTriggerOverlay;
7540
+ const PHONE = '(max-width: 767px)';
7541
+ const HEADER = '[data-phase] header';
7542
+ /** Pill → the official trigger it fronts. Same ARIA split the pills use. */
7543
+ const PAIRS = [
7544
+ { pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[aria-haspopup="tree"]` },
7545
+ { pill: '[data-activity-kind="job"]', trigger: `${HEADER} button[class$="_trigger"]:not([aria-haspopup])` },
7546
+ ];
7547
+ /** Marks a trigger we have taken over, so cleanup can find it again. */
7548
+ const TAKEN = 'data-zen-overlay';
7549
+ function place(trigger, pill) {
7550
+ const r = pill.getBoundingClientRect();
7551
+ if (r.width === 0 || r.height === 0)
7552
+ return;
7553
+ trigger.setAttribute(TAKEN, '');
7554
+ // Fixed, not absolute: the header's containing block is not the pill's, and
7555
+ // fixed rects are exactly what the portalled popover measures against.
7556
+ trigger.style.setProperty('position', 'fixed', 'important');
7557
+ trigger.style.setProperty('left', `${r.left}px`, 'important');
7558
+ trigger.style.setProperty('top', `${r.top}px`, 'important');
7559
+ trigger.style.setProperty('width', `${r.width}px`, 'important');
7560
+ trigger.style.setProperty('height', `${r.height}px`, 'important');
7561
+ trigger.style.setProperty('margin', '0', 'important');
7562
+ trigger.style.setProperty('padding', '0', 'important');
7563
+ trigger.style.setProperty('border', '0', 'important');
7564
+ trigger.style.setProperty('opacity', '0', 'important');
7565
+ trigger.style.setProperty('overflow', 'hidden', 'important');
7566
+ trigger.style.setProperty('pointer-events', 'auto', 'important');
7567
+ trigger.style.setProperty('z-index', '3', 'important');
7568
+ }
7569
+ function release(trigger) {
7570
+ trigger.removeAttribute(TAKEN);
7571
+ trigger.style.cssText = '';
7572
+ }
7573
+ function installNativeTriggerOverlay(ctx) {
7574
+ ctx.effect(() => {
7575
+ const phone = window.matchMedia(PHONE);
7576
+ let frame = 0;
7577
+ const sync = () => {
7578
+ for (const { pill, trigger } of PAIRS) {
7579
+ const t = document.querySelector(trigger);
7580
+ if (t === null)
7581
+ continue;
7582
+ const p = document.querySelector(pill);
7583
+ // No pill (count zero, or desktop): hand the trigger back untouched.
7584
+ if (!phone.matches || p === null) {
7585
+ if (t.hasAttribute(TAKEN))
7586
+ release(t);
7587
+ continue;
7588
+ }
7589
+ place(t, p);
7590
+ }
7591
+ };
7592
+ /* Layout settles a frame late after a re-render, and a stale rect would
7593
+ leave the tap target next to the pill instead of on it. */
7594
+ const schedule = () => {
7595
+ if (frame !== 0)
7596
+ return;
7597
+ frame = requestAnimationFrame(() => { frame = 0; sync(); });
7598
+ };
7599
+ const observer = new MutationObserver(schedule);
7600
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-activity-state', 'data-phase'] });
7601
+ window.addEventListener('resize', schedule);
7602
+ phone.addEventListener('change', schedule);
7603
+ schedule();
7604
+ return () => {
7605
+ if (frame !== 0)
7606
+ cancelAnimationFrame(frame);
7607
+ observer.disconnect();
7608
+ window.removeEventListener('resize', schedule);
7609
+ phone.removeEventListener('change', schedule);
7610
+ for (const t of document.querySelectorAll(`[${TAKEN}]`))
7611
+ release(t);
7612
+ };
7613
+ }, 'dsh-zen-remote: native trigger overlay');
7614
+ }
7615
+ };
7486
7616
  __modules["effects/welcome-notice.js"] = function (require, module, exports) {
7487
7617
  "use strict";
7488
7618
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -8058,6 +8188,7 @@ const header_status_ts_1 = require("./effects/header-status.js");
8058
8188
  const gestures_ts_1 = require("./effects/gestures.js");
8059
8189
  const turn_fold_ts_1 = require("./effects/turn-fold.js");
8060
8190
  const modal_back_ts_1 = require("./effects/modal-back.js");
8191
+ const native_trigger_overlay_ts_1 = require("./effects/native-trigger-overlay.js");
8061
8192
  const welcome_notice_ts_1 = require("./effects/welcome-notice.js");
8062
8193
  const keyboard_guard_ts_1 = require("./effects/keyboard-guard.js");
8063
8194
  const keyboard_avoid_ts_1 = require("./effects/keyboard-avoid.js");
@@ -8105,6 +8236,7 @@ function apply(ctx) {
8105
8236
  // not carry this.
8106
8237
  (0, turn_fold_ts_1.installTurnFold)(ctx);
8107
8238
  (0, modal_back_ts_1.installModalBack)(ctx);
8239
+ (0, native_trigger_overlay_ts_1.installNativeTriggerOverlay)(ctx);
8108
8240
  // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
8109
8241
  // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
8110
8242
  // per-browser "不再弹出" opt-out instead.
@@ -1 +1 @@
1
- {"version":3,"file":"MobileSessionHeader.d.ts","sourceRoot":"","sources":["../../../src/client/MobileSessionHeader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AA6GjC,yEAAyE;AACzE,MAAM,MAAM,wBAAwB,GAChC,YAAY,CAAC,qCAAqC,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,EAAE,EAAE,iBAAiB,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAQ5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,WAAW,EAAE,CAe3C;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,wBAAwB,+BA4E1F;AAED,sEAAsE;AACtE,MAAM,MAAM,0BAA0B,GAClC,YAAY,CAAC,uCAAuC,CAAC,GACrD,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,EAAE,0BAA0B,+BAkGtE"}
1
+ {"version":3,"file":"MobileSessionHeader.d.ts","sourceRoot":"","sources":["../../../src/client/MobileSessionHeader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEjF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAyHjC,yEAAyE;AACzE,MAAM,MAAM,wBAAwB,GAChC,YAAY,CAAC,qCAAqC,CAAC,GACnD,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,EAAE,EAAE,iBAAiB,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,IAAI,WAAW,EAAE,CAQ5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,WAAW,EAAE,CAe3C;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,wBAAwB,+BA4E1F;AAED,sEAAsE;AACtE,MAAM,MAAM,0BAA0B,GAClC,YAAY,CAAC,uCAAuC,CAAC,GACrD,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAE1B;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,EAAE,0BAA0B,+BAkGtE"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lay each official header trigger transparently over the activity pill that
3
+ * stands in for it, so a real finger tap lands on the official element.
4
+ *
5
+ * Why not just forward the tap (what this replaced): DSH 0.1.1's subagent
6
+ * trigger only reacts to TRUSTED input. Measured on device 2026-08-21 —
7
+ * `.click()`, and synthetic `pointerdown`/`mousedown`/`pointerup`, all leave
8
+ * `aria-expanded` at "false"; a CDP-dispatched real touch at the same point
9
+ * opens it. That was verified with every plugin style override stripped off
10
+ * the trigger, so it is the event path, not our CSS. Any design that scripts
11
+ * the click is therefore dead, and this one moves the real control instead.
12
+ *
13
+ * Why this is now cheap: in 0.1.1 the popover portals to <body> as a
14
+ * `position: fixed` layer (`ZKlsPq_menu`, z-index 100) and positions itself
15
+ * from the trigger's viewport rect — measured 336px wide, fully on screen.
16
+ * So placing the trigger over the pill also lands the popover at the pill,
17
+ * and none of the old "park an anchor, re-anchor the menu" CSS is needed.
18
+ *
19
+ * The pill keeps the visuals (icon, count, state dot); the invisible official
20
+ * trigger on top keeps the behaviour. Pills are `pointer-events: none` so a
21
+ * tap can only ever reach the real thing — no double handling.
22
+ */
23
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
24
+ export declare function installNativeTriggerOverlay(ctx: ClientContext): void;
25
+ //# sourceMappingURL=native-trigger-overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-trigger-overlay.d.ts","sourceRoot":"","sources":["../../../../src/client/effects/native-trigger-overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAuC3E,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAuCpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA0B,MAAM,wCAAwC,CAAA;AAsBnG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,sCAAsC;QACtC,WAAW,EAAE,YAAY,CAAA;KAC1B;CACF;AAED,0GAA0G;AAC1G,eAAO,MAAM,MAAM,UAAgF,CAAA;AAEnG;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqM9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA0B,MAAM,wCAAwC,CAAA;AAuBnG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,sCAAsC;QACtC,WAAW,EAAE,YAAY,CAAA;KAC1B;CACF;AAED,0GAA0G;AAC1G,eAAO,MAAM,MAAM,UAAgF,CAAA;AAEnG;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAsM9C"}
@@ -1,2 +1,2 @@
1
- export declare const COMPOSER_CSS = "/* ---------- phone composer (< 768px) ---------- */\n\n/* The file picker the attachment button drives (S7) \u2014 hidden at EVERY width,\n deliberately OUTSIDE the phone media block below. A bare <input type=\"file\">\n renders as a native \"Choose Files\" control, and the slot wrapper around it is\n `display: contents`, so an un-hidden one becomes a flex item of the official\n tool row. Scoping this to < 768px once put that control in the DESKTOP\n composer (caught on a live deploy, 2026-08-17): the button that drives it is\n phone-only, but the input it drives is in the DOM at all widths. */\n[data-mobile-nav=\"attach-picker\"] {\n display: none !important;\n}\n\n@media (max-width: 767px) {\n /* --- 1. flatten the two official groups ---\n `_tools` and `_trailing` only exist to cluster controls left/right.\n display:contents dissolves them into `_row`, so the six leaf controls\n become siblings in one flex line and `order` alone drives the layout:\n [attach \u00B7 + \u00B7 permission \u00B7 model] \u2026elastic gap\u2026 [context ring \u00B7 send]. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] {\n justify-content: flex-start !important;\n gap: 5px !important;\n padding: 2px 8px 8px !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] {\n display: contents !important;\n }\n\n /* --- 2. the running order ---\n The attachment button (S3 placeholder, S7 wires it up) sits leftmost;\n the official \"+\" command menu keeps its seat right next to it, where\n the two \"insert something\" affordances read as one cluster. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] [data-mobile-nav=\"attach\"] {\n order: 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_add\"] {\n order: 2 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] {\n order: 3 !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n gap: 6px !important;\n }\n /* The model seat carries the elastic gap: everything after it is pushed to\n the right edge, so no spacer element is needed. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] {\n order: 4 !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n margin-right: auto !important;\n }\n /* Third-party input.right entries park next to the ring rather than\n landing at order 0 (= far left) once the groups are flattened. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.right\"] > * {\n order: 5 !important;\n }\n /* ContextMeter \u2014 the only `span` ending in _root inside the row. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > span[class$=\"_root\"] {\n order: 6 !important;\n flex: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n order: 7 !important;\n }\n\n /* --- 3. permission + model as icon-only pills (real-device round 2, 2026-08-17) ---\n S3's icon-and-label capsules read as noise on an actual phone \u2014 there\n is no room to usefully show a preset name or a model id, so the label\n text is now hidden outright and both triggers collapse to a plain\n ~44x30 icon button. This AGREES with (rather than fights) the official\n container query (`@container (width <= 460px) { .trigger:has(.triggerIcon)\n .triggerLabel { display: none } }`) that S3 had to override \u2014 no need\n to override it back. Accessible name is unaffected: both official\n triggers already ship a descriptive aria-label independent of the\n visible text (PermissionSelect: t('input.accessMode', {name}); the\n model trigger: t('trigger.aria'/'trigger.ariaEffort')) \u2014 verified live\n in dsh-client-ui-conversation / dsh-client-ui-model-selection lib/\n client.js, 2026-08-17 \u2014 so hiding the label costs nothing for screen\n readers. The permission trigger only renders an icon for the\n \"read-only\" / \"workspace-write\" presets (permissionGlyphs in\n dsh-client-ui-conversation) \u2014 \"Full access\" and any host-configured\n preset name fall back to chevron-only, a known gap in the official\n markup this plugin cannot fill without inventing new icon meaning.\n The model trigger never renders an icon at all (only label + optional\n effort text + chevron), so its ::before below draws one from a\n primitives icon path (Sparkle \u2014 the closest existing \"model\" glyph) as\n a CSS-only pseudo-element: it survives React re-renders for free\n (unlike a DOM-injected node, which would need a MutationObserver, see\n the preview-full-toggle pitfall in AGENTS.md) and does not touch\n accessible-name computation (empty generated content). S3's\n rtl-ellipsis trick on the model label is simply inert under\n display:none now; left alone rather than unpicked. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [class$=\"_triggerLabel\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerLabel\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerEffort\"] {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n width: 44px !important;\n height: 30px !important;\n max-width: none !important;\n min-width: 0 !important;\n padding: 0 !important;\n gap: 2px !important;\n justify-content: center !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n }\n /* PermissionSelect wraps its trigger in the Menu primitive's root span,\n which must shrink to the icon button's fixed width. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] > span:has(> button[class$=\"_trigger\"]) {\n flex: 0 0 auto !important;\n min-width: 0 !important;\n }\n /* ic_ds_sparkle_16 (@deepseek-ai/dsh-client-ui-primitives IconSparkle16\n path, copied verbatim) as a mask so it inherits currentColor like every\n other icon in the row \u2014 the model trigger has no official icon slot to\n hook into. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"]::before {\n content: '';\n width: 16px;\n height: 16px;\n flex: none;\n background: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n }\n\n /* --- 4. both menus become bottom sheets ---\n The permission menu is the Menu primitive (role=menu, absolute, side=top)\n and the model menu is ModelSelect's own `_menu` (absolute, bottom+right).\n Neither has a transformed ancestor between it and the viewport, so\n position:fixed re-anchors both to the screen edge. Only the shell moves \u2014\n the items, the two-level model panes and every selection handler stay\n official. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n transform: none !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n z-index: 60 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n /* 44pt+ rows in both sheets (Menu items, model options, and the model\n sheet's two root cells that drill into the model / effort panes). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] [role=\"menuitem\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] [class$=\"_option\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] [class$=\"_cell\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n /* --- 4b. the official scroll-to-bottom button must not poke through the\n sheet (real-device follow-up, 2026-08-17) ---\n ChatView's own \"jump to latest\" button (aria-label t(\"chat.toBottom\"))\n is `position: sticky; z-index: 8` inside the message column, not\n `position: fixed` \u2014 it never escapes to the same top-level stacking\n context our sheets get promoted to, so raising the sheet's z-index\n further does nothing (measured live: it still rendered on top at\n z-index 60 vs 8). Rather than chase engine-specific stacking-context\n semantics (Chromium and WebKit do not always agree here \u2014 see AGENTS.md\n CDP/document.hidden lesson for another instance of that), this hides\n the button outright while either sheet is open and lets it reappear on\n close: a plain `display: none` behind a live `:has()` read is correct\n regardless of which stacking rules the engine happens to apply.\n Selector: no hashed classes (dsh-client-ui-conversation lib/client.js,\n verified 2026-08-17) \u2014 `data-chat-flow` is the one stable attribute on\n the message column, and the button's wrapper is its only sibling\n (ChatView only ever renders the column and, conditionally, this one\n slot), so the adjacent-sibling combinator pins it precisely. */\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"]) [data-chat-flow] + div,\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"]) [data-chat-flow] + div {\n display: none !important;\n }\n /* --- 5. input box: two lines minimum, five lines maximum ---\n The official autosizer drives the box off the hidden mirror's height, so\n a min-height on the mirror IS the min-height of the field; the scroll cap\n rides the official custom property. 52px = 2 x 24px line box + 4px pad,\n 124px = 5 lines. The hero card keeps its own one-line collapse (misc.css\n pins _scroll/_grow while the placeholder shows). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] [class$=\"_mirror\"] {\n min-height: 52px !important;\n }\n [class$=\"_composerSeat\"] {\n --dsh-composer-text-max-height: 124px;\n }\n\n /* --- 6. no divider above OR below the message list ---\n Instead of a rule the messages butt against, the message scroller\n fades out over its last 26px (S3, no divider above the composer) AND\n fades in over its first 20px (S3.1 real-device round 2: the header's\n own bottom line is removed too, styles/header.css.ts's\n `header::after` rule). The mask lives on the scroll body (NOT on the\n header or the composer): a mask clips everything it paints, and both\n surfaces host position:fixed children (composer's permission/model\n sheets, any future header overlay) that must not be clipped.\n `mask-image` can only be declared once per element, so both fades are\n ONE linear-gradient rather than two separate declarations (the second\n would silently replace the first) \u2014 this is the merge of what used to\n be S3's bottom-only mask. */\n [class$=\"_scrollBody\"] {\n -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);\n mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);\n }\n [class$=\"_composerSeat\"],\n [class$=\"_composerStack\"] {\n border-top: none !important;\n }\n\n /* --- 7. dock entries become mini chips above the input card ---\n conversation.input.dock is display:contents (inline style), so its\n entries are stacked rows of the composer column. Forcing the slot itself\n to flex turns them into ONE scrollable chip line outside and above the\n card. Written against the slot, not against any particular plugin's chip\n (the git branch chip and the todo panel are third-party and may not be\n installed at all). */\n [data-slot=\"conversation.input.dock\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n gap: 6px !important;\n min-width: 0 !important;\n margin: 0 16px !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n scrollbar-width: none !important;\n }\n [data-slot=\"conversation.input.dock\"]::-webkit-scrollbar {\n display: none !important;\n }\n [data-slot=\"conversation.input.dock\"] > * {\n flex: 0 0 auto !important;\n max-width: 70% !important;\n min-height: 26px !important;\n max-height: 26px !important;\n border-radius: 999px !important;\n font-size: 11.5px !important;\n line-height: 18px !important;\n overflow: hidden !important;\n }\n /* The git-graph chip carries a 34px tablet target from misc.css; that\n selector is more specific, so restate it at the phone breakpoint. */\n [data-slot=\"conversation.input.dock\"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {\n min-height: 26px !important;\n padding: 0 10px !important;\n font-size: 11.5px !important;\n }\n /* --- 7a. the native to-do card opts OUT of the 26px pill cage ---\n conversation.input.dock is not only a chip rail: DSH's own TodoPanel\n (dsh-client-ui-conversation) registers here, and it is an expandable\n card, not a pill \u2014 tapping its header renders the to-do list INSIDE\n itself. The cage's max-height:26px + overflow:hidden left that list\n rendered but clipped to nothing, so on a phone the bar looked dead:\n aria-expanded flipped, 8 rows mounted at 216px, and the card still\n painted 26px tall (measured live, 2026-08-20). Give it the official\n geometry back \u2014 full row, natural height, 12px radius. Its list caps\n itself at 180px with its own scroller, so this cannot run into the\n composer. Anchored on the official data-testid, a stable contract\n marker rather than a hashed class name. */\n [data-slot=\"conversation.input.dock\"] > [data-testid=\"todo-panel\"] {\n flex: 1 1 auto !important;\n min-width: 0 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n /* The cage shrinks dock text to 11.5px/18px; the card's rows inherit it\n and turn cramped. Restore the official 13px body scale. */\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* --- 7b. attachment chips (S7.1) ---\n Our own dock entry opts OUT of the 26px pill cage above: an image chip is\n a 48px tile. The row sits tight under the card's top edge, so the chips\n read as part of the composer rather than as a floating strip.\n\n It also takes the LAST line of its own (line policy in 7c): a 48px tile\n beside the to-do chip on one nowrap line squeezed that chip down to an\n unreadable stub, with a long file name pushing the rest off-screen\n (reported 2026-08-22). Its own chips wrap rather than overflow, so\n several attachments stack instead of scrolling out of reach. */\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"attach-chips\"] {\n display: flex !important;\n flex: 1 0 100% !important;\n flex-wrap: wrap !important;\n order: 2 !important;\n align-items: center !important;\n gap: 6px !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 0 !important;\n overflow: visible !important;\n margin-bottom: -2px;\n }\n\n /* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---\n Third entry that is not a pill. conversation.input.dock is a `list` slot\n and DSH's own QueueDock registers into it at order 20 (id `queue`,\n dsh-client-ui-conversation) \u2014 a 36px row per queued message, or a\n collapsible \"N \u6761\u6392\u961F\u6D88\u606F\" header once there is more than one. The 26px\n cage above clipped it to a sliver: on a phone a queued message showed as\n a grey stub poking out from behind the input card, unreadable and with\n its edit / delete / send buttons cut off (reported 2026-08-22, same\n failure the to-do card hit in 7a).\n\n Two things are restored. Geometry: the official row height back, and the\n dock's own composer-column sizing dropped \u2014 its width/max-width are\n calc()s over the composer card variables and its bottom margin is\n negative by design (upstream tucks the strip UNDER the card), none of\n which survives being a flex item of our chip rail.\n\n Layout \u2014 the line policy for the whole rail, now that three things want\n room on it. The pills (the to-do card, a branch chip, whatever else\n registers) keep order 0 and share the top line, still scrolling\n horizontally. The queue takes the next line (order 1). The attachment\n chips take the last one (order 2, 7b), so what the user is about to send\n sits closest to the input it will be sent from. The rail only wraps\n while one of the two full-width entries exists, so a session with\n neither keeps the original single scrolling chip line. */\n [data-slot=\"conversation.input.dock\"]:has(> [data-queue-dock], > [data-mobile-nav=\"attach-chips\"]) {\n flex-wrap: wrap !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] {\n flex: 1 0 100% !important;\n order: 1 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n /* The panel is the dock's only child; upstream rounds its top corners only\n and drops the bottom border, because there it is half-hidden behind the\n card. On its own line it is a free-standing strip, so close it up. */\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > * {\n border-radius: 12px !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > *::after {\n border-bottom: 1px solid var(--dsw-alias-border-l1) !important;\n }\n\n [data-mobile-nav=\"attach-chip\"] {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n max-width: 62vw;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n }\n [data-mobile-nav=\"attach-chip\"][data-kind=\"file\"] {\n height: 26px;\n padding: 0 4px 0 8px;\n border-radius: 999px;\n font-size: 11.5px;\n line-height: 18px;\n }\n /* An image chip is the thumbnail: no name line, the filename lives in the\n title attribute (and in the draft text right below). */\n [data-mobile-nav=\"attach-chip\"][data-kind=\"image\"] {\n width: 48px;\n height: 48px;\n padding: 0;\n border-radius: 10px;\n overflow: hidden;\n }\n [data-mobile-nav=\"attach-chip-art\"] {\n position: relative;\n display: grid;\n place-items: center;\n flex: none;\n overflow: hidden;\n }\n [data-kind=\"file\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 16px;\n height: 16px;\n opacity: .7;\n }\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 100%;\n height: 100%;\n }\n /* The <img> is stacked over the paperclip, not swapped for it: a format the\n engine cannot decode (HEIC outside WebKit) simply paints nothing and the\n icon shows through \u2014 no onError state to carry. */\n [data-mobile-nav=\"attach-chip-art\"] img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .18));\n }\n [data-mobile-nav=\"attach-chip-name\"] {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n }\n [data-mobile-nav=\"attach-chip-remove\"] {\n display: grid;\n place-items: center;\n flex: none;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n touch-action: manipulation;\n }\n /* On a tile the \u00D7 floats in the corner over the picture, so it needs its own\n ground to stay legible against an arbitrary photo. */\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-remove\"] {\n position: absolute;\n top: 2px;\n right: 2px;\n background: rgba(0, 0, 0, .55);\n color: #fff;\n }\n [data-mobile-nav=\"attach-chip-remove\"]:active {\n transform: scale(.9);\n }\n\n /* --- 8. the official stats strip leaves the composer ---\n Its data moves into the session info card (S4). The row is the composer\n dock's own `_root` entry; the slot itself stays live for later entries. */\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] {\n display: none !important;\n }\n\n /* --- 9. the attachment button (S7; its file input is hidden at the top of\n this file, at every width) --- */\n [data-mobile-nav=\"attach\"] {\n position: relative;\n width: 28px !important;\n height: 28px !important;\n flex: none !important;\n display: grid !important;\n place-items: center !important;\n padding: 0 !important;\n border: none !important;\n border-radius: 999px !important;\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n }\n [data-mobile-nav=\"attach\"]:active {\n transform: scale(.94);\n transition: transform .12s;\n }\n /* Busy: a ring sweeps around the paperclip. Drawn with a conic gradient in\n a pseudo-element rather than a spinner node, so the official React tree\n around us has nothing extra to re-render (same reasoning as the model\n pill's mask icon, S3.1). */\n [data-mobile-nav=\"attach\"][data-busy]::after {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 999px;\n background: conic-gradient(from 0deg, transparent 0 65%, currentColor 100%);\n mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n animation: mnav-attach-spin .9s linear infinite;\n pointer-events: none;\n }\n @keyframes mnav-attach-spin {\n to { transform: rotate(1turn); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"attach\"][data-busy]::after {\n animation-duration: 3s;\n }\n }\n /* Failure note: one line above the button, tap-to-dismiss. Sits on the\n composer card (z 2) rather than in an overlay layer \u2014 nothing here needs\n to clear the permission/model sheets. */\n [data-mobile-nav=\"attach-error\"] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 0;\n z-index: 2;\n max-width: 62vw;\n padding: 5px 8px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1.3;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, .82));\n color: var(--dsw-alias-label-primary, #fff);\n box-shadow: 0 2px 8px rgba(0, 0, 0, .24);\n }\n\n /* --- 9. home-indicator clearance (S4.1, 2026-08-17) ---\n Owned HERE, not by the gateway half. That plugin used to carry\n [data-slot=\"conversation.composer\"] { padding-bottom: max(env(safe-area-inset-bottom), 8px) }\n in both pwa/app.css and the gateway's inline DEVICE_CSS. Both were\n INERT and had always been: that slot element is `display: contents`\n (measured 2026-08-17 \u2014 the slot wrapper generates no box, so padding on\n it is discarded), which is why raising it never moved anything. The rule\n is deleted on the PWA side and restated here on an element that actually\n lays out, reading --mnav-sab so ?mobile-nav-inset=54,34 can regress it\n off-device (env() is hard 0 on desktop \u2014 the whole reason S2.1 exists).\n\n Target: the card should look EQUALLY inset on all four sides. This same\n element carries the card's side inset (`padding: 0 16px` officially,\n measured 16px left and 16px right at 390px), so the bottom gap is simply\n capped at that same 16px. Clearing the FULL 34px inset \u2014 the naive\n max(sab, 8px) \u2014 is what read as \"too thick\": it is more than double the\n side margin, so the card looked shoved up off the bottom edge.\n\n clamp() says all three requirements at once:\n - floor 8px -> a device with no home indicator (sab: 0) keeps the\n official 8px exactly, so this is a strict no-op there;\n - track sab -> a shallower inset than 16px is honoured as-is;\n - cap 16px -> a full-size indicator (sab: 34) lands on 16px, equal\n to the side inset, which is the look being asked for.\n Keep the 16px in step with the side padding above if that ever changes;\n that equality IS the spec here, not a coincidence. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n padding-bottom: clamp(8px, var(--mnav-sab), 16px) !important;\n }\n\n /* S10 keyboard avoid: effects/keyboard-avoid.ts mirrors the band of the\n layout viewport hidden behind the software keyboard into --mnav-kb-lift\n and stamps the html attribute while (and only while) it is non-zero \u2014\n a resting transform, even translateY(0), would silently become the\n containing block for any fixed-position descendant of the bar. In every\n environment where the browser's own focus-reveal works the attribute is\n simply never set.\n The target is the slot's _root CHILD, not the slot wrapper: the wrapper\n is display:contents \u2014 it generates no box, so a transform on it is a\n silent no-op (found live on-device via CDP, 2026-08-21; the variable and\n attribute were set and nothing moved). */\n html[data-mnav-kb] [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n transform: translateY(calc(-1 * var(--mnav-kb-lift, 0px)));\n }\n}\n\n/* The attachment button and its preview row only exist for the phone shell.\n Both render at every width (the slots are not breakpoint-aware), so both\n need hiding here \u2014 the dock-row rules that shape the chips live in the\n < 768px block and would otherwise leave a bare, unstyled chip line in the\n desktop composer. */\n@media (min-width: 768px) {\n [data-mobile-nav=\"attach\"],\n [data-mobile-nav=\"attach-chips\"] {\n display: none !important;\n }\n}\n";
1
+ export declare const COMPOSER_CSS = "/* ---------- phone composer (< 768px) ---------- */\n\n/* The file picker the attachment button drives (S7) \u2014 hidden at EVERY width,\n deliberately OUTSIDE the phone media block below. A bare <input type=\"file\">\n renders as a native \"Choose Files\" control, and the slot wrapper around it is\n `display: contents`, so an un-hidden one becomes a flex item of the official\n tool row. Scoping this to < 768px once put that control in the DESKTOP\n composer (caught on a live deploy, 2026-08-17): the button that drives it is\n phone-only, but the input it drives is in the DOM at all widths. */\n[data-mobile-nav=\"attach-picker\"] {\n display: none !important;\n}\n\n@media (max-width: 767px) {\n /* --- 1. flatten the two official groups ---\n `_tools` and `_trailing` only exist to cluster controls left/right.\n display:contents dissolves them into `_row`, so the six leaf controls\n become siblings in one flex line and `order` alone drives the layout:\n [attach \u00B7 + \u00B7 permission \u00B7 model] \u2026elastic gap\u2026 [context ring \u00B7 send]. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] {\n justify-content: flex-start !important;\n gap: 5px !important;\n padding: 2px 8px 8px !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] {\n display: contents !important;\n }\n\n /* --- 2. the running order ---\n The attachment button (S3 placeholder, S7 wires it up) sits leftmost;\n the official \"+\" command menu keeps its seat right next to it, where\n the two \"insert something\" affordances read as one cluster. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] [data-mobile-nav=\"attach\"] {\n order: 1 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_add\"] {\n order: 2 !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] {\n order: 3 !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n gap: 6px !important;\n }\n /* The model seat carries the elastic gap: everything after it is pushed to\n the right edge, so no spacer element is needed. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] {\n order: 4 !important;\n flex: 0 1 auto !important;\n min-width: 0 !important;\n margin-right: auto !important;\n }\n /* Third-party input.right entries park next to the ring rather than\n landing at order 0 (= far left) once the groups are flattened. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.right\"] > * {\n order: 5 !important;\n }\n /* ContextMeter \u2014 the only `span` ending in _root inside the row. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > span[class$=\"_root\"] {\n order: 6 !important;\n flex: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [class$=\"_primary\"] {\n order: 7 !important;\n }\n\n /* --- 3. permission + model as icon-only pills (real-device round 2, 2026-08-17) ---\n S3's icon-and-label capsules read as noise on an actual phone \u2014 there\n is no room to usefully show a preset name or a model id, so the label\n text is now hidden outright and both triggers collapse to a plain\n ~44x30 icon button. This AGREES with (rather than fights) the official\n container query (`@container (width <= 460px) { .trigger:has(.triggerIcon)\n .triggerLabel { display: none } }`) that S3 had to override \u2014 no need\n to override it back. Accessible name is unaffected: both official\n triggers already ship a descriptive aria-label independent of the\n visible text (PermissionSelect: t('input.accessMode', {name}); the\n model trigger: t('trigger.aria'/'trigger.ariaEffort')) \u2014 verified live\n in dsh-client-ui-conversation / dsh-client-ui-model-selection lib/\n client.js, 2026-08-17 \u2014 so hiding the label costs nothing for screen\n readers. The permission trigger only renders an icon for the\n \"read-only\" / \"workspace-write\" presets (permissionGlyphs in\n dsh-client-ui-conversation) \u2014 \"Full access\" and any host-configured\n preset name fall back to chevron-only, a known gap in the official\n markup this plugin cannot fill without inventing new icon meaning.\n The model trigger never renders an icon at all (only label + optional\n effort text + chevron), so its ::before below draws one from a\n primitives icon path (Sparkle \u2014 the closest existing \"model\" glyph) as\n a CSS-only pseudo-element: it survives React re-renders for free\n (unlike a DOM-injected node, which would need a MutationObserver, see\n the preview-full-toggle pitfall in AGENTS.md) and does not touch\n accessible-name computation (empty generated content). S3's\n rtl-ellipsis trick on the model label is simply inert under\n display:none now; left alone rather than unpicked. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [class$=\"_triggerLabel\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerLabel\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] > [class$=\"_triggerEffort\"] {\n display: none !important;\n }\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"] {\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n width: 44px !important;\n height: 30px !important;\n max-width: none !important;\n min-width: 0 !important;\n padding: 0 !important;\n gap: 2px !important;\n justify-content: center !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n }\n /* PermissionSelect wraps its trigger in the Menu primitive's root span,\n which must shrink to the icon button's fixed width. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] > span:has(> button[class$=\"_trigger\"]) {\n flex: 0 0 auto !important;\n min-width: 0 !important;\n }\n /* ic_ds_sparkle_16 (@deepseek-ai/dsh-client-ui-primitives IconSparkle16\n path, copied verbatim) as a mask so it inherits currentColor like every\n other icon in the row \u2014 the model trigger has no official icon slot to\n hook into. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_trigger\"]::before {\n content: '';\n width: 16px;\n height: 16px;\n flex: none;\n background: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n }\n\n /* --- 4. both menus become bottom sheets ---\n The permission menu is the Menu primitive (role=menu, absolute, side=top)\n and the model menu is ModelSelect's own `_menu` (absolute, bottom+right).\n Neither has a transformed ancestor between it and the viewport, so\n position:fixed re-anchors both to the screen edge. Only the shell moves \u2014\n the items, the two-level model panes and every selection handler stay\n official. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] {\n position: fixed !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n top: auto !important;\n transform: none !important;\n width: 100% !important;\n min-width: 0 !important;\n max-width: none !important;\n max-height: min(70dvh, 520px) !important;\n box-sizing: border-box !important;\n border-radius: 16px 16px 0 0 !important;\n border-bottom: none !important;\n padding: 8px 8px calc(8px + var(--mnav-sab)) !important;\n z-index: 60 !important;\n box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;\n }\n /* 44pt+ rows in both sheets (Menu items, model options, and the model\n sheet's two root cells that drill into the model / effort panes). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] [role=\"menuitem\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] [class$=\"_option\"],\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"] [class$=\"_cell\"] {\n min-height: 48px !important;\n border-radius: 12px !important;\n font-size: 15px !important;\n }\n /* --- 4b. the official scroll-to-bottom button must not poke through the\n sheet (real-device follow-up, 2026-08-17) ---\n ChatView's own \"jump to latest\" button (aria-label t(\"chat.toBottom\"))\n is `position: sticky; z-index: 8` inside the message column, not\n `position: fixed` \u2014 it never escapes to the same top-level stacking\n context our sheets get promoted to, so raising the sheet's z-index\n further does nothing (measured live: it still rendered on top at\n z-index 60 vs 8). Rather than chase engine-specific stacking-context\n semantics (Chromium and WebKit do not always agree here \u2014 see AGENTS.md\n CDP/document.hidden lesson for another instance of that), this hides\n the button outright while either sheet is open and lets it reappear on\n close: a plain `display: none` behind a live `:has()` read is correct\n regardless of which stacking rules the engine happens to apply.\n Selector: no hashed classes (dsh-client-ui-conversation lib/client.js,\n verified 2026-08-17) \u2014 `data-chat-flow` is the one stable attribute on\n the message column, and the button's wrapper is its only sibling\n (ChatView only ever renders the column and, conditionally, this one\n slot), so the adjacent-sibling combinator pins it precisely. */\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"]) [data-chat-flow] + div,\n body:has([data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_trailing\"] > [data-slot=\"conversation.input.model\"] > [class$=\"_root\"] > [class$=\"_menu\"]) [data-chat-flow] + div {\n display: none !important;\n }\n /* --- 5. input box: two lines minimum, five lines maximum ---\n The official autosizer drives the box off the hidden mirror's height, so\n a min-height on the mirror IS the min-height of the field; the scroll cap\n rides the official custom property. 52px = 2 x 24px line box + 4px pad,\n 124px = 5 lines. The hero card keeps its own one-line collapse (misc.css\n pins _scroll/_grow while the placeholder shows). */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] [class$=\"_mirror\"] {\n min-height: 52px !important;\n }\n [class$=\"_composerSeat\"] {\n --dsh-composer-text-max-height: 124px;\n }\n\n /* --- 6. no divider above OR below the message list ---\n Instead of a rule the messages butt against, they fade near both edges.\n This used to be ONE mask-image on the message scroller; iOS WebKit\n turned that into fog over the header (user report 2026-08-23): a mask\n on a scroll container is applied in CONTENT coordinates there (the fade\n bands scroll away with the messages) and, worse, it breaks the\n container's own overflow clipping \u2014 content scrolled out of view keeps\n painting, straight over the header and status bar. Engine bug with no\n iOS escape hatch (every iOS browser is WebKit), so the fades moved off\n the scroller entirely:\n\n - fade-IN under the header: the header's own ::after, restyled into a\n 20px gradient strip \u2014 styles/header.css.ts (\"no header bottom line\").\n - fade-OUT above the composer: nothing of ours. The official composer\n seat already carries `background: linear-gradient(transparent 0,\n bg 36px)` on its sticky self (dsh-client-ui-conversation lib/\n client.js .wSkVaW_composerSeat) and that is the entire visible\n effect; the mask's own bottom band landed inside the safe-area\n padding below the card and painted nothing. If the bottom edge ever\n reads harsh, hang a ::before strip off the seat the same way the\n header does \u2014 do NOT put a mask back on the scroller.\n\n Painted strips instead of a mask is a fair trade only because the page\n background is one flat color \u2014 which it is (--dsw-alias-bg-base). */\n [class$=\"_composerSeat\"],\n [class$=\"_composerStack\"] {\n border-top: none !important;\n }\n\n /* --- 7. dock entries become mini chips above the input card ---\n conversation.input.dock is display:contents (inline style), so its\n entries are stacked rows of the composer column. Forcing the slot itself\n to flex turns them into ONE scrollable chip line outside and above the\n card. Written against the slot, not against any particular plugin's chip\n (the git branch chip and the todo panel are third-party and may not be\n installed at all). */\n [data-slot=\"conversation.input.dock\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n gap: 6px !important;\n min-width: 0 !important;\n margin: 0 16px !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n scrollbar-width: none !important;\n }\n [data-slot=\"conversation.input.dock\"]::-webkit-scrollbar {\n display: none !important;\n }\n [data-slot=\"conversation.input.dock\"] > * {\n flex: 0 0 auto !important;\n max-width: 70% !important;\n min-height: 26px !important;\n max-height: 26px !important;\n border-radius: 999px !important;\n font-size: 11.5px !important;\n line-height: 18px !important;\n overflow: hidden !important;\n }\n /* The git-graph chip carries a 34px tablet target from misc.css; that\n selector is more specific, so restate it at the phone breakpoint. */\n [data-slot=\"conversation.input.dock\"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {\n min-height: 26px !important;\n padding: 0 10px !important;\n font-size: 11.5px !important;\n }\n /* --- 7a. the native to-do card opts OUT of the 26px pill cage ---\n conversation.input.dock is not only a chip rail: DSH's own TodoPanel\n (dsh-client-ui-conversation) registers here, and it is an expandable\n card, not a pill \u2014 tapping its header renders the to-do list INSIDE\n itself. The cage's max-height:26px + overflow:hidden left that list\n rendered but clipped to nothing, so on a phone the bar looked dead:\n aria-expanded flipped, 8 rows mounted at 216px, and the card still\n painted 26px tall (measured live, 2026-08-20). Give it the official\n geometry back \u2014 full row, natural height, 12px radius. Its list caps\n itself at 180px with its own scroller, so this cannot run into the\n composer. Anchored on the official data-testid, a stable contract\n marker rather than a hashed class name. */\n [data-slot=\"conversation.input.dock\"] > [data-testid=\"todo-panel\"] {\n flex: 1 1 auto !important;\n min-width: 0 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n overflow: visible !important;\n border-radius: 12px !important;\n /* The cage shrinks dock text to 11.5px/18px; the card's rows inherit it\n and turn cramped. Restore the official 13px body scale. */\n font-size: 13px !important;\n line-height: normal !important;\n }\n\n /* --- 7b. attachment chips (S7.1) ---\n Our own dock entry opts OUT of the 26px pill cage above: an image chip is\n a 48px tile. The row sits tight under the card's top edge, so the chips\n read as part of the composer rather than as a floating strip.\n\n It also takes the LAST line of its own (line policy in 7c): a 48px tile\n beside the to-do chip on one nowrap line squeezed that chip down to an\n unreadable stub, with a long file name pushing the rest off-screen\n (reported 2026-08-22). Its own chips wrap rather than overflow, so\n several attachments stack instead of scrolling out of reach. */\n [data-slot=\"conversation.input.dock\"] > [data-mobile-nav=\"attach-chips\"] {\n display: flex !important;\n flex: 1 0 100% !important;\n flex-wrap: wrap !important;\n order: 2 !important;\n align-items: center !important;\n gap: 6px !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n border-radius: 0 !important;\n overflow: visible !important;\n margin-bottom: -2px;\n }\n\n /* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---\n Third entry that is not a pill. conversation.input.dock is a `list` slot\n and DSH's own QueueDock registers into it at order 20 (id `queue`,\n dsh-client-ui-conversation) \u2014 a 36px row per queued message, or a\n collapsible \"N \u6761\u6392\u961F\u6D88\u606F\" header once there is more than one. The 26px\n cage above clipped it to a sliver: on a phone a queued message showed as\n a grey stub poking out from behind the input card, unreadable and with\n its edit / delete / send buttons cut off (reported 2026-08-22, same\n failure the to-do card hit in 7a).\n\n Two things are restored. Geometry: the official row height back, and the\n dock's own composer-column sizing dropped \u2014 its width/max-width are\n calc()s over the composer card variables and its bottom margin is\n negative by design (upstream tucks the strip UNDER the card), none of\n which survives being a flex item of our chip rail.\n\n Layout \u2014 the line policy for the whole rail, now that three things want\n room on it. The pills (the to-do card, a branch chip, whatever else\n registers) keep order 0 and share the top line, still scrolling\n horizontally. The queue takes the next line (order 1). The attachment\n chips take the last one (order 2, 7b), so what the user is about to send\n sits closest to the input it will be sent from. The rail only wraps\n while one of the two full-width entries exists, so a session with\n neither keeps the original single scrolling chip line. */\n [data-slot=\"conversation.input.dock\"]:has(> [data-queue-dock], > [data-mobile-nav=\"attach-chips\"]) {\n flex-wrap: wrap !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] {\n flex: 1 0 100% !important;\n order: 1 !important;\n max-width: none !important;\n min-height: 0 !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: visible !important;\n font-size: 13px !important;\n line-height: normal !important;\n }\n /* The panel is the dock's only child; upstream rounds its top corners only\n and drops the bottom border, because there it is half-hidden behind the\n card. On its own line it is a free-standing strip, so close it up. */\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > * {\n border-radius: 12px !important;\n }\n [data-slot=\"conversation.input.dock\"] > [data-queue-dock] > *::after {\n border-bottom: 1px solid var(--dsw-alias-border-l1) !important;\n }\n\n [data-mobile-nav=\"attach-chip\"] {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n max-width: 62vw;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n }\n [data-mobile-nav=\"attach-chip\"][data-kind=\"file\"] {\n height: 26px;\n padding: 0 4px 0 8px;\n border-radius: 999px;\n font-size: 11.5px;\n line-height: 18px;\n }\n /* An image chip is the thumbnail: no name line, the filename lives in the\n title attribute (and in the draft text right below). */\n [data-mobile-nav=\"attach-chip\"][data-kind=\"image\"] {\n width: 48px;\n height: 48px;\n padding: 0;\n border-radius: 10px;\n overflow: hidden;\n }\n [data-mobile-nav=\"attach-chip-art\"] {\n position: relative;\n display: grid;\n place-items: center;\n flex: none;\n overflow: hidden;\n }\n [data-kind=\"file\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 16px;\n height: 16px;\n opacity: .7;\n }\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-art\"] {\n width: 100%;\n height: 100%;\n }\n /* The <img> is stacked over the paperclip, not swapped for it: a format the\n engine cannot decode (HEIC outside WebKit) simply paints nothing and the\n icon shows through \u2014 no onError state to carry. */\n [data-mobile-nav=\"attach-chip-art\"] img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .18));\n }\n [data-mobile-nav=\"attach-chip-name\"] {\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n }\n [data-mobile-nav=\"attach-chip-remove\"] {\n display: grid;\n place-items: center;\n flex: none;\n width: 18px;\n height: 18px;\n padding: 0;\n border: none;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n touch-action: manipulation;\n }\n /* On a tile the \u00D7 floats in the corner over the picture, so it needs its own\n ground to stay legible against an arbitrary photo. */\n [data-kind=\"image\"] > [data-mobile-nav=\"attach-chip-remove\"] {\n position: absolute;\n top: 2px;\n right: 2px;\n background: rgba(0, 0, 0, .55);\n color: #fff;\n }\n [data-mobile-nav=\"attach-chip-remove\"]:active {\n transform: scale(.9);\n }\n\n /* --- 8. the official stats strip leaves the composer ---\n Its data moves into the session info card (S4). The row is the composer\n dock's own `_root` entry; the slot itself stays live for later entries. */\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] {\n display: none !important;\n }\n\n /* --- 9. the attachment button (S7; its file input is hidden at the top of\n this file, at every width) --- */\n [data-mobile-nav=\"attach\"] {\n position: relative;\n width: 28px !important;\n height: 28px !important;\n flex: none !important;\n display: grid !important;\n place-items: center !important;\n padding: 0 !important;\n border: none !important;\n border-radius: 999px !important;\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12));\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n }\n [data-mobile-nav=\"attach\"]:active {\n transform: scale(.94);\n transition: transform .12s;\n }\n /* Busy: a ring sweeps around the paperclip. Drawn with a conic gradient in\n a pseudo-element rather than a spinner node, so the official React tree\n around us has nothing extra to re-render (same reasoning as the model\n pill's mask icon, S3.1). */\n [data-mobile-nav=\"attach\"][data-busy]::after {\n content: \"\";\n position: absolute;\n inset: -2px;\n border-radius: 999px;\n background: conic-gradient(from 0deg, transparent 0 65%, currentColor 100%);\n mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));\n animation: mnav-attach-spin .9s linear infinite;\n pointer-events: none;\n }\n @keyframes mnav-attach-spin {\n to { transform: rotate(1turn); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-mobile-nav=\"attach\"][data-busy]::after {\n animation-duration: 3s;\n }\n }\n /* Failure note: one line above the button, tap-to-dismiss. Sits on the\n composer card (z 2) rather than in an overlay layer \u2014 nothing here needs\n to clear the permission/model sheets. */\n [data-mobile-nav=\"attach-error\"] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 0;\n z-index: 2;\n max-width: 62vw;\n padding: 5px 8px;\n border-radius: 8px;\n font-size: 12px;\n line-height: 1.3;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, .82));\n color: var(--dsw-alias-label-primary, #fff);\n box-shadow: 0 2px 8px rgba(0, 0, 0, .24);\n }\n\n /* --- 9. home-indicator clearance (S4.1, 2026-08-17) ---\n Owned HERE, not by the gateway half. That plugin used to carry\n [data-slot=\"conversation.composer\"] { padding-bottom: max(env(safe-area-inset-bottom), 8px) }\n in both pwa/app.css and the gateway's inline DEVICE_CSS. Both were\n INERT and had always been: that slot element is `display: contents`\n (measured 2026-08-17 \u2014 the slot wrapper generates no box, so padding on\n it is discarded), which is why raising it never moved anything. The rule\n is deleted on the PWA side and restated here on an element that actually\n lays out, reading --mnav-sab so ?mobile-nav-inset=54,34 can regress it\n off-device (env() is hard 0 on desktop \u2014 the whole reason S2.1 exists).\n\n Target: the card should look EQUALLY inset on all four sides. This same\n element carries the card's side inset (`padding: 0 16px` officially,\n measured 16px left and 16px right at 390px), so the bottom gap is simply\n capped at that same 16px. Clearing the FULL 34px inset \u2014 the naive\n max(sab, 8px) \u2014 is what read as \"too thick\": it is more than double the\n side margin, so the card looked shoved up off the bottom edge.\n\n clamp() says all three requirements at once:\n - floor 8px -> a device with no home indicator (sab: 0) keeps the\n official 8px exactly, so this is a strict no-op there;\n - track sab -> a shallower inset than 16px is honoured as-is;\n - cap 16px -> a full-size indicator (sab: 34) lands on 16px, equal\n to the side inset, which is the look being asked for.\n Keep the 16px in step with the side padding above if that ever changes;\n that equality IS the spec here, not a coincidence. */\n [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n padding-bottom: clamp(8px, var(--mnav-sab), 16px) !important;\n }\n\n /* S10 keyboard avoid: effects/keyboard-avoid.ts mirrors the band of the\n layout viewport hidden behind the software keyboard into --mnav-kb-lift\n and stamps the html attribute while (and only while) it is non-zero \u2014\n a resting transform, even translateY(0), would silently become the\n containing block for any fixed-position descendant of the bar. In every\n environment where the browser's own focus-reveal works the attribute is\n simply never set.\n The target is the slot's _root CHILD, not the slot wrapper: the wrapper\n is display:contents \u2014 it generates no box, so a transform on it is a\n silent no-op (found live on-device via CDP, 2026-08-21; the variable and\n attribute were set and nothing moved). */\n html[data-mnav-kb] [data-slot=\"conversation.composer.bar\"] > [class$=\"_root\"] {\n transform: translateY(calc(-1 * var(--mnav-kb-lift, 0px)));\n }\n}\n\n/* The attachment button and its preview row only exist for the phone shell.\n Both render at every width (the slots are not breakpoint-aware), so both\n need hiding here \u2014 the dock-row rules that shape the chips live in the\n < 768px block and would otherwise leave a bare, unstyled chip line in the\n desktop composer. */\n@media (min-width: 768px) {\n [data-mobile-nav=\"attach\"],\n [data-mobile-nav=\"attach-chips\"] {\n display: none !important;\n }\n}\n";
2
2
  //# sourceMappingURL=composer.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"composer.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/composer.css.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,YAAY,g36BAqkBxB,CAAA"}
1
+ {"version":3,"file":"composer.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/composer.css.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,YAAY,2x7BA4kBxB,CAAA"}
@@ -1,2 +1,2 @@
1
- export declare const HEADER_CSS = "/* ---------- session header five-piece reflow (< 768px) ---------- */\n\n/* New elements render unconditionally (React does not know about media\n queries); default them to hidden so 768px+ never sees them, then\n re-enable inside the phone block below. Belt-and-braces alongside the\n scoped rules \u2014 mirrors the existing [data-mobile-nav=\"*\"] desktop no-op\n list in misc.css.ts. */\n[data-mobile-nav=\"header-back\"],\n[data-mobile-nav=\"header-viewrow\"],\n[data-mobile-nav=\"header-activity\"],\n[data-mobile-nav=\"header-info\"],\n[data-mobile-nav=\"header-workbench\"],\n/* The workbench close pill's default-hidden used to live in compat.css.ts \u2014\n but EVERYTHING in layout/compat/misc sits inside one shared\n (max-width: 1023px) block (opened at the top of layout, closed at the end\n of misc), so that \"top-level\" rule silently never applied at >=1024 and\n the pill rendered as an unstyled button on desktop (2026-08-17 report).\n This file is appended after the shared block closes, so here it is truly\n global; the <=767px :has() show rule in compat overrides it with\n !important. */\n[data-mobile-nav=\"better-sidebar-close\"] {\n display: none;\n}\n\n@media (max-width: 767px) {\n /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet \u2014\n a different, unrelated suite from the workbench entry this slice adds\n (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as\n v1.0.0 shipped it. */\n [data-mobile-nav=\"files\"] {\n display: none !important;\n }\n\n /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */\n /* The bottom padding is the view-switch row's seat: that row is\n position:absolute (see the bottom of this file \u2014 it renders two levels\n deep inside headerActions and can never be a grid item of titleRow), so\n without a reserve it would hang over the message list. 28px = its\n height. Unconditional rather than header:has([data-mobile-nav=\n \"header-viewrow\"]) on purpose: :has() is silently dropped by pre-105\n WebViews (see AGENTS.md) and a 28px overlap on the first message is a\n worse failure than 28px of empty band in the (never observed in\n practice) single-view session. Total header chrome: 48 + 28 = 76px.\n Left/right 8px (S2.1 report leftover, fixed in S4): the official header\n had zero horizontal padding, so the utilities button cluster's right\n edge sat flush against the screen edge (x=390 at 390px width, no\n margin). The 92px grid columns absorb this fine \u2014 they're fixed track\n widths, only the center 1fr column shrinks by 16px. */\n [data-phase] header {\n position: relative;\n padding: 0 8px 28px !important;\n }\n /* No header bottom line \u2014 fade instead (real-device round 2, 2026-08-17).\n The official header draws its border as a `::after` 1px bar (the\n `border-bottom` on the header itself is transparent, just a layout\n reserve \u2014 dsh-client-ui-conversation lib/client.js \".wSkVaW_header{\n border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;\n position:absolute;bottom:1px...}\", verified live 2026-08-17), so\n hiding the pseudo-element is enough; the header's own border-bottom\n never had a visible color to begin with. The message scroller now\n fades in from under the header instead (styles/composer.css.ts, \"no\n divider above OR below the message list\" \u2014 the two edges share one\n mask-image on [class$=\"_scrollBody\"]). */\n [data-phase] header::after {\n display: none !important;\n }\n /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for\n the \"\u6807\u9898\u88AB\u6324\u4E0B\u53BB\" report). The three items are placed with explicit\n grid-column but arrive in DOM order crumbs(2) \u2192 headerActions(1) \u2192\n headerUtilities(3): sparse auto-placement never moves its cursor\n backwards, so headerActions and headerUtilities were pushed onto an\n implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 =\n back button 44px, titleRow = 72px with the title stuck at the top\n instead of centered. Pinning the row makes it one 48px band again. */\n [data-phase] header [class$=\"_titleRow\"] {\n position: relative;\n display: grid;\n grid-template-columns: 92px 1fr 92px;\n grid-template-rows: minmax(48px, auto);\n align-items: center;\n min-height: 48px;\n padding: 0 !important;\n }\n /* Flatten the official cluster so its two children (crumbs, headerActions)\n become direct grid items of titleRow instead of a nested flex box. */\n [data-phase] header [class$=\"_titleCluster\"] {\n display: contents;\n }\n /* margin-left: 0 cancels layout.css.ts's shared <=1023px\n \"margin-left: auto !important\" on this exact selector (it pushed the\n Files button to the row's right edge in the old non-grid header;\n inside our grid column it instead shoves the back button 48px right of\n the column's start \u2014 the auto margin absorbs the leftover space\n between the button's content width and the 92px column). */\n [data-phase] header [class$=\"_headerActions\"] {\n grid-column: 1;\n grid-row: 1;\n justify-self: start;\n margin-left: 0 !important;\n display: flex;\n align-items: center;\n min-width: 0;\n }\n /* layout.css.ts's shared <=1023px block hides this element outright\n (header > :first-child > :last-child { display: none !important },\n specificity (0,3,1) \u2014 v1.0.0 relocated the desktop Session-log capsule\n to the drawer footer on every narrow width). That selector is repeated\n here at equal specificity so the later source order wins the display\n property; the class-suffix selector alone (0,2,1) would silently lose. */\n [data-phase] header [class$=\"_headerUtilities\"],\n [data-phase] header > :first-child > :last-child {\n grid-column: 3;\n grid-row: 1;\n justify-self: end;\n display: flex !important;\n align-items: center;\n gap: 4px;\n min-width: 0;\n }\n /* justify-self: stretch (the grid default) so this fills the whole 1fr\n column as a definite-width box \u2014 center/center-content happens inside\n it. justify-self: center (tried first) sizes the item to its content\n with no automatic column cap, and the obvious fix \u2014 max-width: 100% \u2014\n resolves against the WRONG box here (measured 93.6px at 390px, where\n the column is ~206px): the item's DOM parent (titleCluster) is\n display:contents, and percentage max-width does not reliably resolve\n through a boxless ancestor to the grid track. Stretching sidesteps the\n percentage entirely \u2014 but layout.css.ts's shared <=1023px block ALSO\n caps this exact selector at max-width: 24vw !important (93.6px at\n 390px \u2014 the value this rule used to silently inherit once its own\n max-width was dropped in favor of stretch), so that must be reset to\n none here too. */\n [data-phase] header [class$=\"_crumbs\"] {\n grid-column: 2;\n grid-row: 1;\n justify-self: stretch;\n max-width: none !important;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n }\n /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header\n lets you tap the parent session's title to switch back \u2014 that affordance\n was lost on phone when the whole parent chain was hidden as \"\u591A\u4F59\u9879\".\n Show only the immediate parent (for agent-teams member sessions that IS\n the captain/main session; deeper ancestors stay hidden \u2014 a 3-level chain\n cannot fit the ~206px center column at 390px). The parent crumb is the\n official breadcrumb <button> (dsh-client-ui-conversation renders\n open(parentId) as its onClick), so tapping needs no JS from this plugin,\n and main sessions (chain length 1) have no :nth-last-child(2) seg at\n all \u2014 the centered single title is pixel-identical to before. */\n [data-phase] header [class$=\"_crumbSeg\"]:not(:nth-last-child(-n+2)) {\n display: none !important;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) {\n display: inline-flex;\n align-items: center;\n flex: none;\n min-width: 0;\n max-width: 38%;\n }\n /* Ancestors BEFORE the parent seg are hidden, which would leave the\n parent seg's own leading \"/\" dangling at the start of the title. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumbSep\"] {\n display: none !important;\n }\n /* Parent crumb: small tertiary text so the 19px current title stays the\n visual anchor; min-height keeps a finger-sized target inside the 48px\n row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumb\"] {\n font-size: 13px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));\n min-height: 44px;\n display: inline-flex;\n align-items: center;\n padding: 0 2px 0 6px;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:last-child {\n display: flex;\n align-items: center;\n min-width: 0;\n max-width: 100%;\n }\n /* The last seg's own \"/\" is the divider between parent and current title \u2014\n it only exists when there IS an ancestor (index > 0), so it can simply\n stay visible now that the parent seg shows. (It used to be hidden here:\n with the parent hidden it dangled before the title.) */\n /* BOTH selectors are required: [class$=] matches the whole class\n ATTRIBUTE's suffix, and the current-session title button's attribute is\n \"wSkVaW_crumb wSkVaW_crumbCurrent\" \u2014 it ends in _crumbCurrent, so the _crumb\n suffix selector alone silently missed the one element that matters\n (measured live: the title stayed 14px while the rule sat in the bundle,\n 2026-08-17). */\n [data-phase] header [class$=\"_crumb\"],\n [data-phase] header [class$=\"_crumbCurrent\"] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n max-width: 100%;\n /* Real-device feedback (2026-08-17): the session title read too small\n next to the enlarged header icons \u2014 19px sits just under the home\n screen's 22px workspace title, keeping the two-level hierarchy. */\n font-size: 19px;\n font-weight: 600;\n }\n\n /* Running-status dot: no official element to reposition (the header\n renders only the crumb title, the two action slots, and the tablist \u2014\n no status indicator), so this is the plan's documented fallback \u2014\n read the data ourselves and draw it (effects/header-status.ts stamps\n data-mobile-nav-dot on the frame). A ::after on the title crumb keeps\n it truly inline with the (centered, truncated) text instead of a\n separately positioned box that would fight the centering math. */\n [data-phase] header [class$=\"_crumbCurrent\"]::after {\n content: '';\n display: none;\n width: 6px;\n height: 6px;\n margin-left: 6px;\n border-radius: 50%;\n vertical-align: middle;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot] header [class$=\"_crumbCurrent\"]::after {\n display: inline-block;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"ongoing\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"warning\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"done\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n\n /* Everything else the official header puts in these two list slots \u2014 the\n agent-preset mode badge, the jobs/subagent trigger, the desktop\n Session-log download capsule, and this plugin's own legacy directory\n toggle \u2014 is the \"\u4E0D\u5E38\u9A7B\" set the design defers to the S4 info card.\n Scoped by the slots' own [data-slot] wrapper (a stable, non-hashed\n contract marker) rather than enumerating each registrant, so a future\n third-party header action is hidden by default too. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > *,\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > * {\n display: none !important;\n }\n /* Native background-task / subagent entries: kept mounted, but parked as\n ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.\n Our activity pill is the visible control and forwards the tap to the\n official trigger, so the official popover \u2014 a child of this root \u2014 still\n mounts and positions from here.\n\n Why not just leave them visible in the header: each is ~103px wide while\n headerActions' grid column is 92px, and flex children that cannot shrink\n simply overflow \u2014 measured at 375px the subagent entry ran x=60..163\n straight across the centred title at x=100..275.\n\n Why not display:none them either: the menu lives inside the root, so\n hiding the root takes the popover with it and the pill would have\n nothing to open. Hide the TRIGGER (below) and keep the root as a box. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [class$=\"_root\"]:has(> button[class$=\"_trigger\"]) {\n display: block !important;\n position: absolute !important;\n right: 0 !important;\n top: 100% !important;\n width: 0 !important;\n height: 28px !important;\n overflow: visible !important;\n order: 0 !important;\n }\n /* The official trigger never shows \u2014 the pill replaced it. Programmatic\n .click() still reaches React's handler on a display:none button. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [class$=\"_root\"] > button[class$=\"_trigger\"] {\n display: none !important;\n }\n /* Anchor the popover to the RIGHT edge and clamp it to the viewport. The\n root now sits at the right edge with zero width, so the official\n `left: 0` would start the 336px panel off-screen; layout.css.ts's\n `left: 8px` re-anchor was written for a viewport-width containing block\n that this element is not. Right-anchoring is the one that holds for both\n (2026-08-20: the old pairing put the panel 29px past the right edge). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] [class$=\"_menu\"] {\n left: auto !important;\n right: 0 !important;\n width: min(336px, calc(100vw - 32px)) !important;\n max-width: none !important;\n max-height: min(420px, calc(100dvh - 140px)) !important;\n }\n /* The agent-preset mode badge specifically survives the blanket hide\n above: layout.css.ts's shared <=1023px block targets it directly\n (header [class$=\"_label\"]:has(> svg) { display: block !important }),\n specificity (0,3,1), higher than the [data-slot] wrapper rule (0,2,1) \u2014\n display wins on specificity before source order, so a same-selector\n override is needed here too. */\n [data-phase] header [class$=\"_label\"]:has(> svg) {\n display: none !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-back\"] {\n display: inline-flex !important;\n }\n /* header-viewrow is position:absolute (see below), so its re-shown\n display value must match the flex layout its own rule declares. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-viewrow\"] {\n display: flex !important;\n }\n /* Our own chip lives in the same slot, so the blanket hide above swallows\n it unless it is named here too (it was, on the first build: the chip\n rendered with the right counts and painted nothing). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-activity\"] {\n display: inline-flex !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-info\"],\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex !important;\n }\n\n /* Back button: 44pt touch target, leftmost column. */\n [data-mobile-nav=\"header-back\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-back\"]:active {\n opacity: .6;\n }\n\n /* Info + workbench utility buttons, rightmost column. */\n [data-mobile-nav=\"header-info\"],\n [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-info\"]:active,\n [data-mobile-nav=\"header-workbench\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n /* Icon family unification (real-device round 2, 2026-08-17): the \u24D8 text\n glyph is gone (MobileSessionHeader.tsx now renders IconInfoOutline16,\n a local 16px SVG built to match the primitives icon family) and the\n workbench button's IconPanelLeftOutline16 is mirrored into a\n panel-RIGHT glyph \u2014 there's no IconPanelRightOutline16 in primitives\n (checked lib/types/icons/index.d.ts), and the plugin's own right-side\n panel semantics are exactly the left icon flipped. Both buttons now\n carry a same-size (16px), same-stroke-weight icon. */\n [data-mobile-nav=\"header-workbench\"] svg {\n transform: scaleX(-1);\n }\n\n /* Official Chat/Trajectory tablist: removed from layout entirely (S2.1 \u2014\n visibility:hidden still held a 27px row, so the header carried the\n view-switch row's band TWICE: 72 + 27 + gap = 104px measured at 390px,\n and a real iPhone added ~54px of notch on top of that). The header's own\n padding-bottom above is now the row's seat.\n display:none does NOT break the view switch: HTMLElement.click()\n dispatches synthetically and fires React's handler regardless of\n rendering \u2014 only real pointer hit-testing needs a box, and this plugin\n never relies on it (MobileSessionHeader always calls .click()). */\n [data-phase] header [class$=\"_tabs\"][role=\"tablist\"] {\n display: none;\n }\n\n /* View-switch row: \"current view name + dots\", replacing the (still\n present, still clickable) official tablist visually. It renders inside\n headerActions/headerUtilities, but titleRow's own position:relative\n (set above) makes IT the containing block for absolute descendants \u2014\n display:contents on titleCluster does not break that search \u2014 so\n top:100% sits directly under the title regardless of the title row's\n actual height, with no hardcoded offset to keep in sync. */\n /* Activity chip (subagents / background tasks) rides the right end of the\n same band. It is a SIBLING of the switch button, not a child \u2014 that\n button spans the full row, so nesting would make every tap on the chip\n switch views too \u2014 and it shares titleRow as its containing block, so\n top:100% lands both on the same 28px line. z-index puts it above the\n row it overlaps. */\n [data-mobile-nav=\"header-activity\"] {\n position: absolute;\n right: 0;\n top: 100%;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n gap: 9px;\n height: 28px;\n padding: 0 2px 0 10px;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"activity-pill\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"activity-pill\"]:active {\n opacity: .55;\n }\n [data-mobile-nav=\"activity-count\"] {\n font-variant-numeric: tabular-nums;\n }\n /* Steady green = everything settled. */\n [data-mobile-nav=\"activity-dot\"] {\n display: block;\n width: 6px;\n height: 6px;\n margin-left: 1px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n /* A job that failed or was killed is not a success \u2014 never paint it green. */\n [data-activity-state=\"warning\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n /* Pulsing blue = still running. Same state palette as the session dot. */\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n animation: dsh-zen-activity-pulse 1.4s ease-in-out infinite;\n }\n @keyframes dsh-zen-activity-pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: .45; transform: scale(.72); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n animation: none;\n }\n }\n\n [data-mobile-nav=\"header-viewrow\"] {\n position: absolute;\n left: 0;\n right: 0;\n top: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i {\n display: block;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: currentColor;\n opacity: .35;\n font-style: normal;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i[data-active] {\n opacity: 1;\n }\n}\n";
1
+ export declare const HEADER_CSS = "/* ---------- session header five-piece reflow (< 768px) ---------- */\n\n/* New elements render unconditionally (React does not know about media\n queries); default them to hidden so 768px+ never sees them, then\n re-enable inside the phone block below. Belt-and-braces alongside the\n scoped rules \u2014 mirrors the existing [data-mobile-nav=\"*\"] desktop no-op\n list in misc.css.ts. */\n[data-mobile-nav=\"header-back\"],\n[data-mobile-nav=\"header-viewrow\"],\n[data-mobile-nav=\"header-activity\"],\n[data-mobile-nav=\"header-info\"],\n[data-mobile-nav=\"header-workbench\"],\n/* The workbench close pill's default-hidden used to live in compat.css.ts \u2014\n but EVERYTHING in layout/compat/misc sits inside one shared\n (max-width: 1023px) block (opened at the top of layout, closed at the end\n of misc), so that \"top-level\" rule silently never applied at >=1024 and\n the pill rendered as an unstyled button on desktop (2026-08-17 report).\n This file is appended after the shared block closes, so here it is truly\n global; the <=767px :has() show rule in compat overrides it with\n !important. */\n[data-mobile-nav=\"better-sidebar-close\"] {\n display: none;\n}\n\n@media (max-width: 767px) {\n /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet \u2014\n a different, unrelated suite from the workbench entry this slice adds\n (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as\n v1.0.0 shipped it. */\n [data-mobile-nav=\"files\"] {\n display: none !important;\n }\n\n /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */\n /* The bottom padding is the view-switch row's seat: that row is\n position:absolute (see the bottom of this file \u2014 it renders two levels\n deep inside headerActions and can never be a grid item of titleRow), so\n without a reserve it would hang over the message list. 28px = its\n height. Unconditional rather than header:has([data-mobile-nav=\n \"header-viewrow\"]) on purpose: :has() is silently dropped by pre-105\n WebViews (see AGENTS.md) and a 28px overlap on the first message is a\n worse failure than 28px of empty band in the (never observed in\n practice) single-view session. Total header chrome: 48 + 28 = 76px.\n Left/right 8px (S2.1 report leftover, fixed in S4): the official header\n had zero horizontal padding, so the utilities button cluster's right\n edge sat flush against the screen edge (x=390 at 390px width, no\n margin). The 92px grid columns absorb this fine \u2014 they're fixed track\n widths, only the center 1fr column shrinks by 16px. */\n [data-phase] header {\n position: relative;\n /* Above the scroller's plain and relative-positioned message content, so\n the ::after fade strip below actually paints over the messages sliding\n under it (positioned z-auto boxes otherwise win by DOM order \u2014 the\n scroller comes after the header). Deliberately tiny: the seat (7),\n scroll-to-bottom (8) and every overlay this plugin owns (30+) stay\n above. */\n z-index: 2;\n padding: 0 8px 28px !important;\n }\n /* No header bottom line \u2014 fade instead (real-device round 2, 2026-08-17).\n The official header draws its border as a `::after` 1px bar (the\n `border-bottom` on the header itself is transparent, just a layout\n reserve \u2014 dsh-client-ui-conversation lib/client.js \".wSkVaW_header{\n border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;\n position:absolute;bottom:1px...}\", verified live 2026-08-17), so the\n bar is repurposed rather than hidden: restyled into a 20px strip\n hanging just below the header that fades the messages in \u2014 the same\n look the scroller's mask-image used to produce, without a mask on a\n scroll container (iOS WebKit renders that as fog over the header, see\n composer.css.ts \"no divider above OR below the message list\",\n 2026-08-23). color-mix instead of the transparent keyword: the host\n writes its own seat gradient that way, and plain `transparent` is\n rgba(0,0,0,0) \u2014 WebKit interpolates it through grey. */\n [data-phase] header::after {\n height: 20px !important;\n top: 100% !important;\n bottom: auto !important;\n background: linear-gradient(to bottom, var(--dsw-alias-bg-base), color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent)) !important;\n }\n /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for\n the \"\u6807\u9898\u88AB\u6324\u4E0B\u53BB\" report). The three items are placed with explicit\n grid-column but arrive in DOM order crumbs(2) \u2192 headerActions(1) \u2192\n headerUtilities(3): sparse auto-placement never moves its cursor\n backwards, so headerActions and headerUtilities were pushed onto an\n implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 =\n back button 44px, titleRow = 72px with the title stuck at the top\n instead of centered. Pinning the row makes it one 48px band again. */\n [data-phase] header [class$=\"_titleRow\"] {\n position: relative;\n display: grid;\n grid-template-columns: 92px 1fr 92px;\n grid-template-rows: minmax(48px, auto);\n align-items: center;\n min-height: 48px;\n padding: 0 !important;\n }\n /* Flatten the official cluster so its two children (crumbs, headerActions)\n become direct grid items of titleRow instead of a nested flex box. */\n [data-phase] header [class$=\"_titleCluster\"] {\n display: contents;\n }\n /* margin-left: 0 cancels layout.css.ts's shared <=1023px\n \"margin-left: auto !important\" on this exact selector (it pushed the\n Files button to the row's right edge in the old non-grid header;\n inside our grid column it instead shoves the back button 48px right of\n the column's start \u2014 the auto margin absorbs the leftover space\n between the button's content width and the 92px column). */\n [data-phase] header [class$=\"_headerActions\"] {\n grid-column: 1;\n grid-row: 1;\n justify-self: start;\n margin-left: 0 !important;\n display: flex;\n align-items: center;\n min-width: 0;\n }\n /* layout.css.ts's shared <=1023px block hides this element outright\n (header > :first-child > :last-child { display: none !important },\n specificity (0,3,1) \u2014 v1.0.0 relocated the desktop Session-log capsule\n to the drawer footer on every narrow width). That selector is repeated\n here at equal specificity so the later source order wins the display\n property; the class-suffix selector alone (0,2,1) would silently lose. */\n [data-phase] header [class$=\"_headerUtilities\"],\n [data-phase] header > :first-child > :last-child {\n grid-column: 3;\n grid-row: 1;\n justify-self: end;\n display: flex !important;\n align-items: center;\n gap: 4px;\n min-width: 0;\n }\n /* justify-self: stretch (the grid default) so this fills the whole 1fr\n column as a definite-width box \u2014 center/center-content happens inside\n it. justify-self: center (tried first) sizes the item to its content\n with no automatic column cap, and the obvious fix \u2014 max-width: 100% \u2014\n resolves against the WRONG box here (measured 93.6px at 390px, where\n the column is ~206px): the item's DOM parent (titleCluster) is\n display:contents, and percentage max-width does not reliably resolve\n through a boxless ancestor to the grid track. Stretching sidesteps the\n percentage entirely \u2014 but layout.css.ts's shared <=1023px block ALSO\n caps this exact selector at max-width: 24vw !important (93.6px at\n 390px \u2014 the value this rule used to silently inherit once its own\n max-width was dropped in favor of stretch), so that must be reset to\n none here too. */\n [data-phase] header [class$=\"_crumbs\"] {\n grid-column: 2;\n grid-row: 1;\n justify-self: stretch;\n max-width: none !important;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n }\n /* Subagent sessions keep ONE ancestor crumb (2026-08-18): the PC header\n lets you tap the parent session's title to switch back \u2014 that affordance\n was lost on phone when the whole parent chain was hidden as \"\u591A\u4F59\u9879\".\n Show only the immediate parent (for agent-teams member sessions that IS\n the captain/main session; deeper ancestors stay hidden \u2014 a 3-level chain\n cannot fit the ~206px center column at 390px). The parent crumb is the\n official breadcrumb <button> (dsh-client-ui-conversation renders\n open(parentId) as its onClick), so tapping needs no JS from this plugin,\n and main sessions (chain length 1) have no :nth-last-child(2) seg at\n all \u2014 the centered single title is pixel-identical to before. */\n [data-phase] header [class$=\"_crumbSeg\"]:not(:nth-last-child(-n+2)) {\n display: none !important;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) {\n display: inline-flex;\n align-items: center;\n flex: none;\n min-width: 0;\n max-width: 38%;\n }\n /* Ancestors BEFORE the parent seg are hidden, which would leave the\n parent seg's own leading \"/\" dangling at the start of the title. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumbSep\"] {\n display: none !important;\n }\n /* Parent crumb: small tertiary text so the 19px current title stays the\n visual anchor; min-height keeps a finger-sized target inside the 48px\n row. Specificity (0,4,1) outranks the shared 19px _crumb rule below. */\n [data-phase] header [class$=\"_crumbSeg\"]:nth-last-child(2) [class$=\"_crumb\"] {\n font-size: 13px;\n font-weight: 400;\n color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .45));\n min-height: 44px;\n display: inline-flex;\n align-items: center;\n padding: 0 2px 0 6px;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-phase] header [class$=\"_crumbSeg\"]:last-child {\n display: flex;\n align-items: center;\n min-width: 0;\n max-width: 100%;\n }\n /* The last seg's own \"/\" is the divider between parent and current title \u2014\n it only exists when there IS an ancestor (index > 0), so it can simply\n stay visible now that the parent seg shows. (It used to be hidden here:\n with the parent hidden it dangled before the title.) */\n /* BOTH selectors are required: [class$=] matches the whole class\n ATTRIBUTE's suffix, and the current-session title button's attribute is\n \"wSkVaW_crumb wSkVaW_crumbCurrent\" \u2014 it ends in _crumbCurrent, so the _crumb\n suffix selector alone silently missed the one element that matters\n (measured live: the title stayed 14px while the rule sat in the bundle,\n 2026-08-17). */\n [data-phase] header [class$=\"_crumb\"],\n [data-phase] header [class$=\"_crumbCurrent\"] {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n max-width: 100%;\n /* Real-device feedback (2026-08-17): the session title read too small\n next to the enlarged header icons \u2014 19px sits just under the home\n screen's 22px workspace title, keeping the two-level hierarchy. */\n font-size: 19px;\n font-weight: 600;\n }\n\n /* Running-status dot: no official element to reposition (the header\n renders only the crumb title, the two action slots, and the tablist \u2014\n no status indicator), so this is the plan's documented fallback \u2014\n read the data ourselves and draw it (effects/header-status.ts stamps\n data-mobile-nav-dot on the frame). A ::after on the title crumb keeps\n it truly inline with the (centered, truncated) text instead of a\n separately positioned box that would fight the centering math. */\n [data-phase] header [class$=\"_crumbCurrent\"]::after {\n content: '';\n display: none;\n width: 6px;\n height: 6px;\n margin-left: 6px;\n border-radius: 50%;\n vertical-align: middle;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot] header [class$=\"_crumbCurrent\"]::after {\n display: inline-block;\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"ongoing\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"warning\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n [data-mobile-nav=\"frame\"][data-mobile-nav-dot=\"done\"] header [class$=\"_crumbCurrent\"]::after {\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n\n /* Everything else the official header puts in these two list slots \u2014 the\n agent-preset mode badge, the jobs/subagent trigger, the desktop\n Session-log download capsule, and this plugin's own legacy directory\n toggle \u2014 is the \"\u4E0D\u5E38\u9A7B\" set the design defers to the S4 info card.\n Scoped by the slots' own [data-slot] wrapper (a stable, non-hashed\n contract marker) rather than enumerating each registrant, so a future\n third-party header action is hidden by default too. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > *,\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > * {\n display: none !important;\n }\n /* Native background-task / subagent entries (rewritten for DSH 0.1.1,\n 2026-08-21). Three upstream changes killed the old \"park the root as a\n zero-width anchor, hide the trigger, forward the tap\" design at once:\n\n - the subagent entry moved out of header.actions into the new\n conversation.session.header.lineage slot (inside the breadcrumb);\n - its root's class attribute is now \"ZKlsPq_root \" \u2014 with a TRAILING\n SPACE \u2014 so a [class$=\"_root\"] anchor matches nothing at all;\n - the trigger only reacts to TRUSTED input: .click() and synthetic\n pointer/mouse events leave aria-expanded untouched (measured on device\n with every plugin override stripped), so forwarding a tap in JS can\n never open it again.\n\n The trigger's geometry is therefore owned at runtime by\n effects/native-trigger-overlay.ts, which lays the REAL button\n transparently over the activity pill \u2014 a genuine finger tap lands on the\n official element. Nothing in this stylesheet may size, hide, or set\n pointer-events on the trigger: that is exactly what would take the tap\n target away again.\n\n CSS keeps only two chores. Dissolve the root so its box (and its\n stray separator, hidden below) stops occupying the breadcrumb, and let\n the trigger participate from wherever the overlay pins it. And no menu\n re-anchoring anymore: the 0.1.1 popover portals to <body> as\n position:fixed and positions itself from the trigger's viewport rect\n (measured 336px wide, fully on screen at 411px). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > *:has(> button[class$=\"_trigger\"]),\n [data-phase] header [data-slot=\"conversation.session.header.lineage\"] > *:has(> button[class$=\"_trigger\"]) {\n display: contents !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] [class$=\"_separator\"],\n [data-phase] header [data-slot=\"conversation.session.header.lineage\"] [class$=\"_separator\"] {\n display: none !important;\n }\n /* The agent-preset mode badge specifically survives the blanket hide\n above: layout.css.ts's shared <=1023px block targets it directly\n (header [class$=\"_label\"]:has(> svg) { display: block !important }),\n specificity (0,3,1), higher than the [data-slot] wrapper rule (0,2,1) \u2014\n display wins on specificity before source order, so a same-selector\n override is needed here too. */\n [data-phase] header [class$=\"_label\"]:has(> svg) {\n display: none !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-back\"] {\n display: inline-flex !important;\n }\n /* header-viewrow is position:absolute (see below), so its re-shown\n display value must match the flex layout its own rule declares. */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-viewrow\"] {\n display: flex !important;\n }\n /* Our own chip lives in the same slot, so the blanket hide above swallows\n it unless it is named here too (it was, on the first build: the chip\n rendered with the right counts and painted nothing). */\n [data-phase] header [data-slot=\"conversation.session.header.actions\"] > [data-mobile-nav=\"header-activity\"] {\n display: inline-flex !important;\n }\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-info\"],\n [data-phase] header [data-slot=\"conversation.session.header.utilities\"] > [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex !important;\n }\n\n /* Back button: 44pt touch target, leftmost column. */\n [data-mobile-nav=\"header-back\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-back\"]:active {\n opacity: .6;\n }\n\n /* Info + workbench utility buttons, rightmost column. */\n [data-mobile-nav=\"header-info\"],\n [data-mobile-nav=\"header-workbench\"] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-info\"]:active,\n [data-mobile-nav=\"header-workbench\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n /* Icon family unification (real-device round 2, 2026-08-17): the \u24D8 text\n glyph is gone (MobileSessionHeader.tsx now renders IconInfoOutline16,\n a local 16px SVG built to match the primitives icon family) and the\n workbench button's IconPanelLeftOutline16 is mirrored into a\n panel-RIGHT glyph \u2014 there's no IconPanelRightOutline16 in primitives\n (checked lib/types/icons/index.d.ts), and the plugin's own right-side\n panel semantics are exactly the left icon flipped. Both buttons now\n carry a same-size (16px), same-stroke-weight icon. */\n [data-mobile-nav=\"header-workbench\"] svg {\n transform: scaleX(-1);\n }\n\n /* Official Chat/Trajectory tablist: removed from layout entirely (S2.1 \u2014\n visibility:hidden still held a 27px row, so the header carried the\n view-switch row's band TWICE: 72 + 27 + gap = 104px measured at 390px,\n and a real iPhone added ~54px of notch on top of that). The header's own\n padding-bottom above is now the row's seat.\n display:none does NOT break the view switch: HTMLElement.click()\n dispatches synthetically and fires React's handler regardless of\n rendering \u2014 only real pointer hit-testing needs a box, and this plugin\n never relies on it (MobileSessionHeader always calls .click()). */\n [data-phase] header [class$=\"_tabs\"][role=\"tablist\"] {\n display: none;\n }\n\n /* View-switch row: \"current view name + dots\", replacing the (still\n present, still clickable) official tablist visually. It renders inside\n headerActions/headerUtilities, but titleRow's own position:relative\n (set above) makes IT the containing block for absolute descendants \u2014\n display:contents on titleCluster does not break that search \u2014 so\n top:100% sits directly under the title regardless of the title row's\n actual height, with no hardcoded offset to keep in sync. */\n /* Activity chip (subagents / background tasks) rides the right end of the\n same band. It is a SIBLING of the switch button, not a child \u2014 that\n button spans the full row, so nesting would make every tap on the chip\n switch views too \u2014 and it shares titleRow as its containing block, so\n top:100% lands both on the same 28px line. z-index puts it above the\n row it overlaps. */\n [data-mobile-nav=\"header-activity\"] {\n position: absolute;\n right: 0;\n top: 100%;\n z-index: 1;\n display: inline-flex;\n align-items: center;\n gap: 9px;\n height: 28px;\n padding: 0 2px 0 10px;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n /* pointer-events:none so the invisible official trigger laid over this pill\n (effects/native-trigger-overlay.ts) is what a tap reaches. The pill is\n visuals only. */\n [data-mobile-nav=\"activity-pill\"] {\n pointer-events: none;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n font-family: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"activity-pill\"]:active {\n opacity: .55;\n }\n [data-mobile-nav=\"activity-count\"] {\n font-variant-numeric: tabular-nums;\n }\n /* Steady green = everything settled. */\n [data-mobile-nav=\"activity-dot\"] {\n display: block;\n width: 6px;\n height: 6px;\n margin-left: 1px;\n border-radius: 50%;\n background: var(--dsw-alias-state-success-primary, #16a34a);\n }\n /* A job that failed or was killed is not a success \u2014 never paint it green. */\n [data-activity-state=\"warning\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-warn-primary, #d97706);\n }\n /* Pulsing blue = still running. Same state palette as the session dot. */\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n background: var(--dsw-alias-state-business-primary, #4f6ef7);\n animation: dsh-zen-activity-pulse 1.4s ease-in-out infinite;\n }\n @keyframes dsh-zen-activity-pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: .45; transform: scale(.72); }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-activity-state=\"running\"] [data-mobile-nav=\"activity-dot\"] {\n animation: none;\n }\n }\n\n [data-mobile-nav=\"header-viewrow\"] {\n position: absolute;\n left: 0;\n right: 0;\n top: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 28px;\n padding: 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));\n font-family: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i {\n display: block;\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: currentColor;\n opacity: .35;\n font-style: normal;\n }\n [data-mobile-nav=\"header-viewrow-dots\"] > i[data-active] {\n opacity: 1;\n }\n}\n";
2
2
  //# sourceMappingURL=header.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"header.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/header.css.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,+2tBA2ftB,CAAA"}
1
+ {"version":3,"file":"header.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/header.css.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,8rvBAogBtB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "把 DeepSeek Harness 变成手机 App:移动端界面重排(两级页面栈、会话列表主屏、composer 重排、手势、附件上传)+ 配对码认证网关 + PWA 安装 + 真 Web Push。Complete mobile plugin for DeepSeek Harness (DSH): app-shell mobile UI, pairing-code gateway for public-internet access behind your own reverse proxy, installable PWA, offline service worker, and real Web Push agent-done notifications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -106,18 +106,18 @@
106
106
  }
107
107
  },
108
108
  "devDependencies": {
109
- "@deepseek-ai/dsh-api-remotes": "0.1.0-rc.7",
110
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
111
- "@deepseek-ai/dsh-host-apiproxy": "0.1.0-rc.7",
112
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
113
- "@deepseek-ai/dsh-session": "0.1.0-rc.7",
114
- "@deepseek-ai/dsh-session-stats": "0.1.0-rc.7",
115
- "@deepseek-ai/dsh-token-meter": "0.1.0-rc.7",
109
+ "@deepseek-ai/dsh-api-remotes": "0.1.1-rc.2",
110
+ "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
111
+ "@deepseek-ai/dsh-host-apiproxy": "0.1.1-rc.2",
112
+ "@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
113
+ "@deepseek-ai/dsh-session": "0.1.1-rc.2",
114
+ "@deepseek-ai/dsh-session-stats": "0.1.1-rc.2",
115
+ "@deepseek-ai/dsh-token-meter": "0.1.1-rc.2",
116
116
  "@types/node": "^26.2.0",
117
117
  "@types/react": "~18.3.1",
118
118
  "@types/react-dom": "~18.3.0",
119
119
  "typescript": "^6.0.3",
120
- "@deepseek-ai/dsh-tools": "0.1.0-rc.7"
120
+ "@deepseek-ai/dsh-tools": "0.1.1-rc.2"
121
121
  },
122
122
  "scripts": {
123
123
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
@@ -60,20 +60,30 @@ function IconTaskOutline14({ size = 14 }: { size?: number }) {
60
60
  type ActivityState = 'running' | 'done' | 'warning'
61
61
 
62
62
  /**
63
- * The official triggers our pills stand in for. Both are hidden and parked
64
- * as invisible anchors at the right end of the view-switch band
65
- * (styles/header.css.ts) so their own popovers still mount and position from
63
+ * The official triggers our pills stand in for. Both are hidden and parked as
64
+ * invisible anchors at the right end of the view-switch band
65
+ * (styles/header.css.ts) so their popovers still mount and position from
66
66
  * there; the pill is the visible control and forwards the tap.
67
67
  *
68
- * Told apart by `aria-haspopup`, not by class: the subagent entry declares
69
- * `tree` (its menu is a session tree) while the jobs entry declares none.
70
- * Both roots/triggers only expose build-hashed class names, so the ARIA
71
- * contract is the one stable discriminator between two otherwise identical
72
- * `[class$="_root"] > button[class$="_trigger"]` shapes.
68
+ * Told apart by `aria-haspopup`: the subagent entry declares `tree` (its menu
69
+ * is a session tree), the jobs entry declares none.
70
+ *
71
+ * Scoped to the HEADER, not to a slot (2026-08-21). DSH 0.1.1 moved the
72
+ * subagent entry out of `conversation.session.header.actions` into a new
73
+ * `conversation.session.header.lineage` slot inside the breadcrumb; every
74
+ * slot-scoped lookup here silently stopped matching and the pill fell back to
75
+ * opening the info card. The header is the stable boundary — which slot
76
+ * inside it owns the entry is the suite's business, and it has changed once.
77
+ *
78
+ * Also deliberately NOT keyed on the root's class. In 0.1.1 that attribute is
79
+ * `"ZKlsPq_root "` — with a TRAILING SPACE — so `[class$="_root"]` does not
80
+ * match it at all (`[class$=]` tests the whole attribute string). The
81
+ * trigger's own class has no such tail, so anchor on the trigger and reach
82
+ * the root through `:has()` instead of naming it.
73
83
  */
74
- const ACTIONS_SLOT = '[data-slot="conversation.session.header.actions"]'
75
- const SUBAGENT_TRIGGER = `${ACTIONS_SLOT} button[aria-haspopup="tree"]`
76
- const JOBS_TRIGGER = `${ACTIONS_SLOT} > [class$="_root"] > button[class$="_trigger"]:not([aria-haspopup])`
84
+ const HEADER = '[data-phase] header'
85
+ const SUBAGENT_TRIGGER = `${HEADER} button[aria-haspopup="tree"]`
86
+ const JOBS_TRIGGER = `${HEADER} button[class$="_trigger"]:not([aria-haspopup])`
77
87
 
78
88
  /** One glanceable group: icon + count + state dot; opens the official popover. */
79
89
  function ActivityPill(
@@ -98,10 +108,12 @@ function ActivityPill(
98
108
  data-activity-state={state}
99
109
  aria-label={label}
100
110
  title={label}
111
+ /* Reached only when the official trigger is absent — when it exists,
112
+ effects/native-trigger-overlay.ts lays it transparently over this
113
+ pill and the tap never gets here. Scripting the official click is
114
+ not an option: 0.1.1 ignores synthetic events (see that file). */
101
115
  onClick={() => {
102
- const el = document.querySelector<HTMLButtonElement>(trigger)
103
- if (el === null) onFallback()
104
- else el.click()
116
+ if (document.querySelector(trigger) === null) onFallback()
105
117
  }}
106
118
  >
107
119
  <Icon />
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Lay each official header trigger transparently over the activity pill that
3
+ * stands in for it, so a real finger tap lands on the official element.
4
+ *
5
+ * Why not just forward the tap (what this replaced): DSH 0.1.1's subagent
6
+ * trigger only reacts to TRUSTED input. Measured on device 2026-08-21 —
7
+ * `.click()`, and synthetic `pointerdown`/`mousedown`/`pointerup`, all leave
8
+ * `aria-expanded` at "false"; a CDP-dispatched real touch at the same point
9
+ * opens it. That was verified with every plugin style override stripped off
10
+ * the trigger, so it is the event path, not our CSS. Any design that scripts
11
+ * the click is therefore dead, and this one moves the real control instead.
12
+ *
13
+ * Why this is now cheap: in 0.1.1 the popover portals to <body> as a
14
+ * `position: fixed` layer (`ZKlsPq_menu`, z-index 100) and positions itself
15
+ * from the trigger's viewport rect — measured 336px wide, fully on screen.
16
+ * So placing the trigger over the pill also lands the popover at the pill,
17
+ * and none of the old "park an anchor, re-anchor the menu" CSS is needed.
18
+ *
19
+ * The pill keeps the visuals (icon, count, state dot); the invisible official
20
+ * trigger on top keeps the behaviour. Pills are `pointer-events: none` so a
21
+ * tap can only ever reach the real thing — no double handling.
22
+ */
23
+
24
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
25
+
26
+ const PHONE = '(max-width: 767px)'
27
+ const HEADER = '[data-phase] header'
28
+
29
+ /** Pill → the official trigger it fronts. Same ARIA split the pills use. */
30
+ const PAIRS: ReadonlyArray<{ pill: string, trigger: string }> = [
31
+ { pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[aria-haspopup="tree"]` },
32
+ { pill: '[data-activity-kind="job"]', trigger: `${HEADER} button[class$="_trigger"]:not([aria-haspopup])` },
33
+ ]
34
+
35
+ /** Marks a trigger we have taken over, so cleanup can find it again. */
36
+ const TAKEN = 'data-zen-overlay'
37
+
38
+ function place(trigger: HTMLElement, pill: Element): void {
39
+ const r = pill.getBoundingClientRect()
40
+ if (r.width === 0 || r.height === 0) return
41
+ trigger.setAttribute(TAKEN, '')
42
+ // Fixed, not absolute: the header's containing block is not the pill's, and
43
+ // fixed rects are exactly what the portalled popover measures against.
44
+ trigger.style.setProperty('position', 'fixed', 'important')
45
+ trigger.style.setProperty('left', `${r.left}px`, 'important')
46
+ trigger.style.setProperty('top', `${r.top}px`, 'important')
47
+ trigger.style.setProperty('width', `${r.width}px`, 'important')
48
+ trigger.style.setProperty('height', `${r.height}px`, 'important')
49
+ trigger.style.setProperty('margin', '0', 'important')
50
+ trigger.style.setProperty('padding', '0', 'important')
51
+ trigger.style.setProperty('border', '0', 'important')
52
+ trigger.style.setProperty('opacity', '0', 'important')
53
+ trigger.style.setProperty('overflow', 'hidden', 'important')
54
+ trigger.style.setProperty('pointer-events', 'auto', 'important')
55
+ trigger.style.setProperty('z-index', '3', 'important')
56
+ }
57
+
58
+ function release(trigger: HTMLElement): void {
59
+ trigger.removeAttribute(TAKEN)
60
+ trigger.style.cssText = ''
61
+ }
62
+
63
+ export function installNativeTriggerOverlay(ctx: ClientContext): void {
64
+ ctx.effect(() => {
65
+ const phone = window.matchMedia(PHONE)
66
+ let frame = 0
67
+
68
+ const sync = (): void => {
69
+ for (const { pill, trigger } of PAIRS) {
70
+ const t = document.querySelector<HTMLElement>(trigger)
71
+ if (t === null) continue
72
+ const p = document.querySelector(pill)
73
+ // No pill (count zero, or desktop): hand the trigger back untouched.
74
+ if (!phone.matches || p === null) {
75
+ if (t.hasAttribute(TAKEN)) release(t)
76
+ continue
77
+ }
78
+ place(t, p)
79
+ }
80
+ }
81
+ /* Layout settles a frame late after a re-render, and a stale rect would
82
+ leave the tap target next to the pill instead of on it. */
83
+ const schedule = (): void => {
84
+ if (frame !== 0) return
85
+ frame = requestAnimationFrame(() => { frame = 0; sync() })
86
+ }
87
+
88
+ const observer = new MutationObserver(schedule)
89
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-activity-state', 'data-phase'] })
90
+ window.addEventListener('resize', schedule)
91
+ phone.addEventListener('change', schedule)
92
+ schedule()
93
+
94
+ return () => {
95
+ if (frame !== 0) cancelAnimationFrame(frame)
96
+ observer.disconnect()
97
+ window.removeEventListener('resize', schedule)
98
+ phone.removeEventListener('change', schedule)
99
+ for (const t of document.querySelectorAll<HTMLElement>(`[${TAKEN}]`)) release(t)
100
+ }
101
+ }, 'dsh-zen-remote: native trigger overlay')
102
+ }
@@ -16,6 +16,7 @@ import { installHeaderStatusDot } from './effects/header-status.ts'
16
16
  import { installGestures } from './effects/gestures.ts'
17
17
  import { installTurnFold } from './effects/turn-fold.ts'
18
18
  import { installModalBack } from './effects/modal-back.ts'
19
+ import { installNativeTriggerOverlay } from './effects/native-trigger-overlay.ts'
19
20
  import { installWelcomeNoticeOptOut } from './effects/welcome-notice.ts'
20
21
  import { installKeyboardGuard } from './effects/keyboard-guard.ts'
21
22
  import { installKeyboardAvoid } from './effects/keyboard-avoid.ts'
@@ -82,6 +83,7 @@ export function apply(ctx: ClientContext): void {
82
83
  // not carry this.
83
84
  installTurnFold(ctx)
84
85
  installModalBack(ctx)
86
+ installNativeTriggerOverlay(ctx)
85
87
 
86
88
  // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
87
89
  // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
@@ -227,22 +227,29 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
227
227
  }
228
228
 
229
229
  /* --- 6. no divider above OR below the message list ---
230
- Instead of a rule the messages butt against, the message scroller
231
- fades out over its last 26px (S3, no divider above the composer) AND
232
- fades in over its first 20px (S3.1 real-device round 2: the header's
233
- own bottom line is removed too, styles/header.css.ts's
234
- \`header::after\` rule). The mask lives on the scroll body (NOT on the
235
- header or the composer): a mask clips everything it paints, and both
236
- surfaces host position:fixed children (composer's permission/model
237
- sheets, any future header overlay) that must not be clipped.
238
- \`mask-image\` can only be declared once per element, so both fades are
239
- ONE linear-gradient rather than two separate declarations (the second
240
- would silently replace the first) this is the merge of what used to
241
- be S3's bottom-only mask. */
242
- [class$="_scrollBody"] {
243
- -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
244
- mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
245
- }
230
+ Instead of a rule the messages butt against, they fade near both edges.
231
+ This used to be ONE mask-image on the message scroller; iOS WebKit
232
+ turned that into fog over the header (user report 2026-08-23): a mask
233
+ on a scroll container is applied in CONTENT coordinates there (the fade
234
+ bands scroll away with the messages) and, worse, it breaks the
235
+ container's own overflow clipping content scrolled out of view keeps
236
+ painting, straight over the header and status bar. Engine bug with no
237
+ iOS escape hatch (every iOS browser is WebKit), so the fades moved off
238
+ the scroller entirely:
239
+
240
+ - fade-IN under the header: the header's own ::after, restyled into a
241
+ 20px gradient strip — styles/header.css.ts ("no header bottom line").
242
+ - fade-OUT above the composer: nothing of ours. The official composer
243
+ seat already carries \`background: linear-gradient(transparent 0,
244
+ bg 36px)\` on its sticky self (dsh-client-ui-conversation lib/
245
+ client.js .wSkVaW_composerSeat) and that is the entire visible
246
+ effect; the mask's own bottom band landed inside the safe-area
247
+ padding below the card and painted nothing. If the bottom edge ever
248
+ reads harsh, hang a ::before strip off the seat the same way the
249
+ header does — do NOT put a mask back on the scroller.
250
+
251
+ Painted strips instead of a mask is a fair trade only because the page
252
+ background is one flat color — which it is (--dsw-alias-bg-base). */
246
253
  [class$="_composerSeat"],
247
254
  [class$="_composerStack"] {
248
255
  border-top: none !important;
@@ -65,6 +65,13 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
65
65
  widths, only the center 1fr column shrinks by 16px. */
66
66
  [data-phase] header {
67
67
  position: relative;
68
+ /* Above the scroller's plain and relative-positioned message content, so
69
+ the ::after fade strip below actually paints over the messages sliding
70
+ under it (positioned z-auto boxes otherwise win by DOM order — the
71
+ scroller comes after the header). Deliberately tiny: the seat (7),
72
+ scroll-to-bottom (8) and every overlay this plugin owns (30+) stay
73
+ above. */
74
+ z-index: 2;
68
75
  padding: 0 8px 28px !important;
69
76
  }
70
77
  /* No header bottom line — fade instead (real-device round 2, 2026-08-17).
@@ -72,14 +79,20 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
72
79
  \`border-bottom\` on the header itself is transparent, just a layout
73
80
  reserve — dsh-client-ui-conversation lib/client.js ".wSkVaW_header{
74
81
  border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;
75
- position:absolute;bottom:1px...}", verified live 2026-08-17), so
76
- hiding the pseudo-element is enough; the header's own border-bottom
77
- never had a visible color to begin with. The message scroller now
78
- fades in from under the header instead (styles/composer.css.ts, "no
79
- divider above OR below the message list" the two edges share one
80
- mask-image on [class$="_scrollBody"]). */
82
+ position:absolute;bottom:1px...}", verified live 2026-08-17), so the
83
+ bar is repurposed rather than hidden: restyled into a 20px strip
84
+ hanging just below the header that fades the messages in the same
85
+ look the scroller's mask-image used to produce, without a mask on a
86
+ scroll container (iOS WebKit renders that as fog over the header, see
87
+ composer.css.ts "no divider above OR below the message list",
88
+ 2026-08-23). color-mix instead of the transparent keyword: the host
89
+ writes its own seat gradient that way, and plain \`transparent\` is
90
+ rgba(0,0,0,0) — WebKit interpolates it through grey. */
81
91
  [data-phase] header::after {
82
- display: none !important;
92
+ height: 20px !important;
93
+ top: 100% !important;
94
+ bottom: auto !important;
95
+ background: linear-gradient(to bottom, var(--dsw-alias-bg-base), color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent)) !important;
83
96
  }
84
97
  /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for
85
98
  the "标题被挤下去" report). The three items are placed with explicit
@@ -267,48 +280,40 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
267
280
  [data-phase] header [data-slot="conversation.session.header.utilities"] > * {
268
281
  display: none !important;
269
282
  }
270
- /* Native background-task / subagent entries: kept mounted, but parked as
271
- ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.
272
- Our activity pill is the visible control and forwards the tap to the
273
- official trigger, so the official popover — a child of this root — still
274
- mounts and positions from here.
283
+ /* Native background-task / subagent entries (rewritten for DSH 0.1.1,
284
+ 2026-08-21). Three upstream changes killed the old "park the root as a
285
+ zero-width anchor, hide the trigger, forward the tap" design at once:
275
286
 
276
- Why not just leave them visible in the header: each is ~103px wide while
277
- headerActions' grid column is 92px, and flex children that cannot shrink
278
- simply overflow measured at 375px the subagent entry ran x=60..163
279
- straight across the centred title at x=100..275.
287
+ - the subagent entry moved out of header.actions into the new
288
+ conversation.session.header.lineage slot (inside the breadcrumb);
289
+ - its root's class attribute is now "ZKlsPq_root " with a TRAILING
290
+ SPACE so a [class$="_root"] anchor matches nothing at all;
291
+ - the trigger only reacts to TRUSTED input: .click() and synthetic
292
+ pointer/mouse events leave aria-expanded untouched (measured on device
293
+ with every plugin override stripped), so forwarding a tap in JS can
294
+ never open it again.
280
295
 
281
- Why not display:none them either: the menu lives inside the root, so
282
- hiding the root takes the popover with it and the pill would have
283
- nothing to open. Hide the TRIGGER (below) and keep the root as a box. */
284
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"]:has(> button[class$="_trigger"]) {
285
- display: block !important;
286
- position: absolute !important;
287
- right: 0 !important;
288
- top: 100% !important;
289
- width: 0 !important;
290
- height: 28px !important;
291
- overflow: visible !important;
292
- order: 0 !important;
293
- }
294
- /* The official trigger never shows — the pill replaced it. Programmatic
295
- .click() still reaches React's handler on a display:none button. */
296
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"] > button[class$="_trigger"] {
296
+ The trigger's geometry is therefore owned at runtime by
297
+ effects/native-trigger-overlay.ts, which lays the REAL button
298
+ transparently over the activity pill a genuine finger tap lands on the
299
+ official element. Nothing in this stylesheet may size, hide, or set
300
+ pointer-events on the trigger: that is exactly what would take the tap
301
+ target away again.
302
+
303
+ CSS keeps only two chores. Dissolve the root so its box (and its
304
+ stray separator, hidden below) stops occupying the breadcrumb, and let
305
+ the trigger participate from wherever the overlay pins it. And no menu
306
+ re-anchoring anymore: the 0.1.1 popover portals to <body> as
307
+ position:fixed and positions itself from the trigger's viewport rect
308
+ (measured 336px wide, fully on screen at 411px). */
309
+ [data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
310
+ [data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
311
+ display: contents !important;
312
+ }
313
+ [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_separator"],
314
+ [data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
297
315
  display: none !important;
298
316
  }
299
- /* Anchor the popover to the RIGHT edge and clamp it to the viewport. The
300
- root now sits at the right edge with zero width, so the official
301
- \`left: 0\` would start the 336px panel off-screen; layout.css.ts's
302
- \`left: 8px\` re-anchor was written for a viewport-width containing block
303
- that this element is not. Right-anchoring is the one that holds for both
304
- (2026-08-20: the old pairing put the panel 29px past the right edge). */
305
- [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_menu"] {
306
- left: auto !important;
307
- right: 0 !important;
308
- width: min(336px, calc(100vw - 32px)) !important;
309
- max-width: none !important;
310
- max-height: min(420px, calc(100dvh - 140px)) !important;
311
- }
312
317
  /* The agent-preset mode badge specifically survives the blanket hide
313
318
  above: layout.css.ts's shared <=1023px block targets it directly
314
319
  (header [class$="_label"]:has(> svg) { display: block !important }),
@@ -433,7 +438,11 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
433
438
  touch-action: manipulation;
434
439
  -webkit-tap-highlight-color: transparent;
435
440
  }
441
+ /* pointer-events:none so the invisible official trigger laid over this pill
442
+ (effects/native-trigger-overlay.ts) is what a tap reaches. The pill is
443
+ visuals only. */
436
444
  [data-mobile-nav="activity-pill"] {
445
+ pointer-events: none;
437
446
  display: inline-flex;
438
447
  align-items: center;
439
448
  gap: 3px;