dsh-zen-remote 1.1.3 → 1.1.5

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.3-5B4CF0?style=flat-square" alt="v1.1.3">
6
+ <img src="https://img.shields.io/badge/release-v1.1.5-5B4CF0?style=flat-square" alt="v1.1.5">
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.3" // 本地开发换成 "link:/path/to/dsh-zen-remote"
42
+ "dsh-zen-remote": "^1.1.5" // 本地开发换成 "link:/path/to/dsh-zen-remote"
43
43
  },
44
44
  "dsh": { "profile": { "bundles": [
45
45
  "@deepseek-ai/dsh-base",
@@ -288,6 +288,7 @@ open http://127.0.0.1:3088/lan-gate/admin
288
288
  | [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats) | 用量与余额入口收进主屏 chips 行 | 0.2.9 |
289
289
  | [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins) | 定时任务入口收进主屏 chips 行 | 0.2.3 |
290
290
  | dsh-at-file | @文件引用,配合附件上传的 `@` 路径引用使用;它和本插件的附件 chip 读同一份草稿 token,手机端隐藏它 `.dsh-uploads/` 下那几行以免同一个文件被画两遍(缩略图 + 文件名),其余 `@` 引用不动 | 0.6.7 |
291
+ | [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market) | 插件市场弹窗顶栏在手机上放不下,关闭的 × 被挤出面板外(触屏没有 Esc,等于关不掉);隐藏标语 / 版本号 / 官网链接三个装饰位,标题改成省略号收缩,语言切换和 × 保留并加大点按面积 | 0.1.66 |
291
292
  | [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit) | 图像 Q&A/OCR,配合手机端附件上传使用 | — |
292
293
  | [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all) | 沿用上游 dsh-web-mobile 的兼容规则(文件树 / 预览浮层限宽居中等) | — |
293
294
 
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
  **新增**
@@ -229,6 +244,7 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
229
244
  - [@ychris12138/dsh-usage-stats](https://github.com/Ychris12138/dsh-usage-stats)(用量与余额,主屏 chips 行可直接打开;旧包名 `dsh-usage-stats` 已废弃,两个名字同时留在 profile 里会因重复的 `usage-stats` 行让 DSH 起不来)——本机实测 **0.2.9**
230
245
  - [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
231
246
  - dsh-at-file(@文件引用;它和本插件的附件 chip 读的是同一份草稿 token,手机端会为一个上传文件同时画出缩略图和文件名两条,故隐藏它 `.dsh-uploads/` 下那几行,其余 @ 引用不动;桌面端不隐藏——那边本插件的 chip 是关掉的,它是唯一的呈现)——本机实测 **0.6.7**
247
+ - [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market)(插件市场;它的弹窗顶栏在手机上放不下,关闭的 × 被挤出面板外,故隐藏标语/版本号/官网链接三个装饰位并让标题省略号收缩,语言切换和 × 保留;认插件自己渲染的 `.dshm-ver[title="@ace-zone/dsh-market"]` 版本片,同名的其他"市场"插件不会被误伤)——本机实测 **0.1.66**
232
248
  - [dsh-vision-toolkit](https://www.npmjs.com/package/@anionex/dsh-vision-toolkit)(图像 Q&A/OCR,配合手机端附件上传使用)
233
249
  - [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all)(文件树 / 预览 / 任务看板 / SSH / 宠物 / 会话统计 / 远程配对 / 设置)——沿用上游兼容规则,本次未扩展
234
250
 
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
  /**
@@ -2464,6 +2476,16 @@ __modules["styles/compat.css.js"] = function (require, module, exports) {
2464
2476
  Object.defineProperty(exports, "__esModule", { value: true });
2465
2477
  exports.COMPAT_CSS = void 0;
2466
2478
  const attach_upload_ts_1 = require("./attach-upload.js");
2479
+ /* Presence gate for @ace-zone/dsh-market. Its markup carries no mount
2480
+ marker and its `dshm-` class prefix is not proof of identity — several
2481
+ unrelated plugins call themselves a "dsh market". The version chip in
2482
+ the modal header is rendered as
2483
+ `<span class="dshm-ver" title="@ace-zone/dsh-market">`, i.e. the exact
2484
+ package name as a rendered contract, so it is the one selector that
2485
+ cannot match anything else. Every rule below hangs off it: with the
2486
+ plugin absent (or its modal closed) nothing matches and the section is
2487
+ inert. */
2488
+ const MARKET = '.dshm-ver[title="@ace-zone/dsh-market"]';
2467
2489
  exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
2468
2490
  The linxin666 plugin suite extends the shell frame directly:
2469
2491
  - aionui-panel appends two trailing grid columns (explorer / preview)
@@ -3230,6 +3252,47 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
3230
3252
  display: none !important;
3231
3253
  }
3232
3254
  }
3255
+ /* ---------- @ace-zone/dsh-market: modal header fits a phone (2026-08-22) ----------
3256
+ THIRD-PARTY COMPAT RULE — @ace-zone/dsh-market (the plugin market:
3257
+ a settings-page entry that opens its own body-level modal,
3258
+ .dshm-overlay > .dshm-panel, over the settings sheet).
3259
+
3260
+ Its header (.dshm-head) is a plain flex row with no wrapping and no
3261
+ shrinking: title, a badge strip whose slogan is white-space:nowrap,
3262
+ a version chip, an "official site" link, a language toggle, and only
3263
+ then the × that closes the modal. Flex items default to
3264
+ min-width:auto, so at phone widths the row keeps its intrinsic size,
3265
+ .dshm-panel clips it (overflow:hidden), and the × — the only way out
3266
+ of the modal on a touch screen, which has no Esc key — is off screen
3267
+ (reported 2026-08-22).
3268
+
3269
+ Drop the three decorative items and let the title ellipsis instead.
3270
+ The language toggle stays: it is a real control, and title + toggle +
3271
+ × fit inside 320px once the title can shrink. The title rule also
3272
+ covers the job / help overlays, which reuse .dshm-head with a long
3273
+ "<action> · <package>" title and no flexible item of their own.
3274
+ Nothing here is desktop-side: at >= 768px the row has the width it
3275
+ was written for. */
3276
+ @media (max-width: 767px) {
3277
+ body:has(${MARKET}) .dshm-head > .dshm-head-badges,
3278
+ body:has(${MARKET}) .dshm-head > .dshm-ver,
3279
+ body:has(${MARKET}) .dshm-head > a.dshm-viewbtn {
3280
+ display: none !important;
3281
+ }
3282
+ body:has(${MARKET}) .dshm-head > .dshm-title {
3283
+ flex: 1 1 auto !important;
3284
+ min-width: 0 !important;
3285
+ overflow: hidden !important;
3286
+ text-overflow: ellipsis !important;
3287
+ white-space: nowrap !important;
3288
+ }
3289
+ /* 44px touch target: their 20px glyph in 2px/6px padding is ~30px. */
3290
+ body:has(${MARKET}) .dshm-head > .dshm-close {
3291
+ flex: 0 0 auto !important;
3292
+ font-size: 24px !important;
3293
+ padding: 6px 12px !important;
3294
+ }
3295
+ }
3233
3296
  `;
3234
3297
  };
3235
3298
  __modules["styles/misc.css.js"] = function (require, module, exports) {
@@ -4126,48 +4189,40 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
4126
4189
  [data-phase] header [data-slot="conversation.session.header.utilities"] > * {
4127
4190
  display: none !important;
4128
4191
  }
4129
- /* Native background-task / subagent entries: kept mounted, but parked as
4130
- ZERO-WIDTH INVISIBLE ANCHORS at the right end of the view-switch band.
4131
- Our activity pill is the visible control and forwards the tap to the
4132
- official trigger, so the official popover — a child of this root — still
4133
- mounts and positions from here.
4134
-
4135
- Why not just leave them visible in the header: each is ~103px wide while
4136
- headerActions' grid column is 92px, and flex children that cannot shrink
4137
- simply overflow measured at 375px the subagent entry ran x=60..163
4138
- straight across the centred title at x=100..275.
4139
-
4140
- Why not display:none them either: the menu lives inside the root, so
4141
- hiding the root takes the popover with it and the pill would have
4142
- nothing to open. Hide the TRIGGER (below) and keep the root as a box. */
4143
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"]:has(> button[class$="_trigger"]) {
4144
- display: block !important;
4145
- position: absolute !important;
4146
- right: 0 !important;
4147
- top: 100% !important;
4148
- width: 0 !important;
4149
- height: 28px !important;
4150
- overflow: visible !important;
4151
- order: 0 !important;
4192
+ /* Native background-task / subagent entries (rewritten for DSH 0.1.1,
4193
+ 2026-08-21). Three upstream changes killed the old "park the root as a
4194
+ zero-width anchor, hide the trigger, forward the tap" design at once:
4195
+
4196
+ - the subagent entry moved out of header.actions into the new
4197
+ conversation.session.header.lineage slot (inside the breadcrumb);
4198
+ - its root's class attribute is now "ZKlsPq_root " with a TRAILING
4199
+ SPACE so a [class$="_root"] anchor matches nothing at all;
4200
+ - the trigger only reacts to TRUSTED input: .click() and synthetic
4201
+ pointer/mouse events leave aria-expanded untouched (measured on device
4202
+ with every plugin override stripped), so forwarding a tap in JS can
4203
+ never open it again.
4204
+
4205
+ The trigger's geometry is therefore owned at runtime by
4206
+ effects/native-trigger-overlay.ts, which lays the REAL button
4207
+ transparently over the activity pill — a genuine finger tap lands on the
4208
+ official element. Nothing in this stylesheet may size, hide, or set
4209
+ pointer-events on the trigger: that is exactly what would take the tap
4210
+ target away again.
4211
+
4212
+ CSS keeps only two chores. Dissolve the root so its box (and its
4213
+ stray separator, hidden below) stops occupying the breadcrumb, and let
4214
+ the trigger participate from wherever the overlay pins it. And no menu
4215
+ re-anchoring anymore: the 0.1.1 popover portals to <body> as
4216
+ position:fixed and positions itself from the trigger's viewport rect
4217
+ (measured 336px wide, fully on screen at 411px). */
4218
+ [data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
4219
+ [data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
4220
+ display: contents !important;
4152
4221
  }
4153
- /* The official trigger never shows — the pill replaced it. Programmatic
4154
- .click() still reaches React's handler on a display:none button. */
4155
- [data-phase] header [data-slot="conversation.session.header.actions"] > [class$="_root"] > button[class$="_trigger"] {
4222
+ [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_separator"],
4223
+ [data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
4156
4224
  display: none !important;
4157
4225
  }
4158
- /* Anchor the popover to the RIGHT edge and clamp it to the viewport. The
4159
- root now sits at the right edge with zero width, so the official
4160
- \`left: 0\` would start the 336px panel off-screen; layout.css.ts's
4161
- \`left: 8px\` re-anchor was written for a viewport-width containing block
4162
- that this element is not. Right-anchoring is the one that holds for both
4163
- (2026-08-20: the old pairing put the panel 29px past the right edge). */
4164
- [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_menu"] {
4165
- left: auto !important;
4166
- right: 0 !important;
4167
- width: min(336px, calc(100vw - 32px)) !important;
4168
- max-width: none !important;
4169
- max-height: min(420px, calc(100dvh - 140px)) !important;
4170
- }
4171
4226
  /* The agent-preset mode badge specifically survives the blanket hide
4172
4227
  above: layout.css.ts's shared <=1023px block targets it directly
4173
4228
  (header [class$="_label"]:has(> svg) { display: block !important }),
@@ -4292,7 +4347,11 @@ exports.HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) -
4292
4347
  touch-action: manipulation;
4293
4348
  -webkit-tap-highlight-color: transparent;
4294
4349
  }
4350
+ /* pointer-events:none so the invisible official trigger laid over this pill
4351
+ (effects/native-trigger-overlay.ts) is what a tap reaches. The pill is
4352
+ visuals only. */
4295
4353
  [data-mobile-nav="activity-pill"] {
4354
+ pointer-events: none;
4296
4355
  display: inline-flex;
4297
4356
  align-items: center;
4298
4357
  gap: 3px;
@@ -7432,6 +7491,108 @@ function installModalBack(ctx) {
7432
7491
  }, 'dsh-zen-remote: modal back gesture');
7433
7492
  }
7434
7493
  };
7494
+ __modules["effects/native-trigger-overlay.js"] = function (require, module, exports) {
7495
+ "use strict";
7496
+ /**
7497
+ * Lay each official header trigger transparently over the activity pill that
7498
+ * stands in for it, so a real finger tap lands on the official element.
7499
+ *
7500
+ * Why not just forward the tap (what this replaced): DSH 0.1.1's subagent
7501
+ * trigger only reacts to TRUSTED input. Measured on device 2026-08-21 —
7502
+ * `.click()`, and synthetic `pointerdown`/`mousedown`/`pointerup`, all leave
7503
+ * `aria-expanded` at "false"; a CDP-dispatched real touch at the same point
7504
+ * opens it. That was verified with every plugin style override stripped off
7505
+ * the trigger, so it is the event path, not our CSS. Any design that scripts
7506
+ * the click is therefore dead, and this one moves the real control instead.
7507
+ *
7508
+ * Why this is now cheap: in 0.1.1 the popover portals to <body> as a
7509
+ * `position: fixed` layer (`ZKlsPq_menu`, z-index 100) and positions itself
7510
+ * from the trigger's viewport rect — measured 336px wide, fully on screen.
7511
+ * So placing the trigger over the pill also lands the popover at the pill,
7512
+ * and none of the old "park an anchor, re-anchor the menu" CSS is needed.
7513
+ *
7514
+ * The pill keeps the visuals (icon, count, state dot); the invisible official
7515
+ * trigger on top keeps the behaviour. Pills are `pointer-events: none` so a
7516
+ * tap can only ever reach the real thing — no double handling.
7517
+ */
7518
+ Object.defineProperty(exports, "__esModule", { value: true });
7519
+ exports.installNativeTriggerOverlay = installNativeTriggerOverlay;
7520
+ const PHONE = '(max-width: 767px)';
7521
+ const HEADER = '[data-phase] header';
7522
+ /** Pill → the official trigger it fronts. Same ARIA split the pills use. */
7523
+ const PAIRS = [
7524
+ { pill: '[data-activity-kind="subagent"]', trigger: `${HEADER} button[aria-haspopup="tree"]` },
7525
+ { pill: '[data-activity-kind="job"]', trigger: `${HEADER} button[class$="_trigger"]:not([aria-haspopup])` },
7526
+ ];
7527
+ /** Marks a trigger we have taken over, so cleanup can find it again. */
7528
+ const TAKEN = 'data-zen-overlay';
7529
+ function place(trigger, pill) {
7530
+ const r = pill.getBoundingClientRect();
7531
+ if (r.width === 0 || r.height === 0)
7532
+ return;
7533
+ trigger.setAttribute(TAKEN, '');
7534
+ // Fixed, not absolute: the header's containing block is not the pill's, and
7535
+ // fixed rects are exactly what the portalled popover measures against.
7536
+ trigger.style.setProperty('position', 'fixed', 'important');
7537
+ trigger.style.setProperty('left', `${r.left}px`, 'important');
7538
+ trigger.style.setProperty('top', `${r.top}px`, 'important');
7539
+ trigger.style.setProperty('width', `${r.width}px`, 'important');
7540
+ trigger.style.setProperty('height', `${r.height}px`, 'important');
7541
+ trigger.style.setProperty('margin', '0', 'important');
7542
+ trigger.style.setProperty('padding', '0', 'important');
7543
+ trigger.style.setProperty('border', '0', 'important');
7544
+ trigger.style.setProperty('opacity', '0', 'important');
7545
+ trigger.style.setProperty('overflow', 'hidden', 'important');
7546
+ trigger.style.setProperty('pointer-events', 'auto', 'important');
7547
+ trigger.style.setProperty('z-index', '3', 'important');
7548
+ }
7549
+ function release(trigger) {
7550
+ trigger.removeAttribute(TAKEN);
7551
+ trigger.style.cssText = '';
7552
+ }
7553
+ function installNativeTriggerOverlay(ctx) {
7554
+ ctx.effect(() => {
7555
+ const phone = window.matchMedia(PHONE);
7556
+ let frame = 0;
7557
+ const sync = () => {
7558
+ for (const { pill, trigger } of PAIRS) {
7559
+ const t = document.querySelector(trigger);
7560
+ if (t === null)
7561
+ continue;
7562
+ const p = document.querySelector(pill);
7563
+ // No pill (count zero, or desktop): hand the trigger back untouched.
7564
+ if (!phone.matches || p === null) {
7565
+ if (t.hasAttribute(TAKEN))
7566
+ release(t);
7567
+ continue;
7568
+ }
7569
+ place(t, p);
7570
+ }
7571
+ };
7572
+ /* Layout settles a frame late after a re-render, and a stale rect would
7573
+ leave the tap target next to the pill instead of on it. */
7574
+ const schedule = () => {
7575
+ if (frame !== 0)
7576
+ return;
7577
+ frame = requestAnimationFrame(() => { frame = 0; sync(); });
7578
+ };
7579
+ const observer = new MutationObserver(schedule);
7580
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-activity-state', 'data-phase'] });
7581
+ window.addEventListener('resize', schedule);
7582
+ phone.addEventListener('change', schedule);
7583
+ schedule();
7584
+ return () => {
7585
+ if (frame !== 0)
7586
+ cancelAnimationFrame(frame);
7587
+ observer.disconnect();
7588
+ window.removeEventListener('resize', schedule);
7589
+ phone.removeEventListener('change', schedule);
7590
+ for (const t of document.querySelectorAll(`[${TAKEN}]`))
7591
+ release(t);
7592
+ };
7593
+ }, 'dsh-zen-remote: native trigger overlay');
7594
+ }
7595
+ };
7435
7596
  __modules["effects/welcome-notice.js"] = function (require, module, exports) {
7436
7597
  "use strict";
7437
7598
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -8007,6 +8168,7 @@ const header_status_ts_1 = require("./effects/header-status.js");
8007
8168
  const gestures_ts_1 = require("./effects/gestures.js");
8008
8169
  const turn_fold_ts_1 = require("./effects/turn-fold.js");
8009
8170
  const modal_back_ts_1 = require("./effects/modal-back.js");
8171
+ const native_trigger_overlay_ts_1 = require("./effects/native-trigger-overlay.js");
8010
8172
  const welcome_notice_ts_1 = require("./effects/welcome-notice.js");
8011
8173
  const keyboard_guard_ts_1 = require("./effects/keyboard-guard.js");
8012
8174
  const keyboard_avoid_ts_1 = require("./effects/keyboard-avoid.js");
@@ -8054,6 +8216,7 @@ function apply(ctx) {
8054
8216
  // not carry this.
8055
8217
  (0, turn_fold_ts_1.installTurnFold)(ctx);
8056
8218
  (0, modal_back_ts_1.installModalBack)(ctx);
8219
+ (0, native_trigger_overlay_ts_1.installNativeTriggerOverlay)(ctx);
8057
8220
  // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
8058
8221
  // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
8059
8222
  // 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 COMPAT_CSS = " /* ---------- dsh-web-ui family compatibility ----------\n The linxin666 plugin suite extends the shell frame directly:\n - aionui-panel appends two trailing grid columns (explorer / preview)\n plus absolute drag handles to [data-dsh-frame]; its 5-track inline\n grid is already overridden above, but the handles and columns would\n still float over the main UI. On mobile the columns leave the grid\n as floating bottom sheets and keep their own visibility state \u2014\n the suite's collapse chevron / preview tabs still work, so no\n feature is lost. The task-board / ssh plugins inject sidebar\n entries and center-column takeover panels; the entries need\n spacing and the kanban needs scrollable columns. */\n\n /* Touch devices: the drag handles are useless \u2014 the floating expand\n button is the opener. */\n .aionui-explorer-handle,\n .aionui-preview-handle {\n display: none !important;\n }\n\n /* Shared base: both columns leave the grid as floating panels. The\n explorer is gated shut by default (its own persisted expanded state\n must never cover the mobile UI on load); the header Files action opens\n it via the frame marker below, and the sheet's own collapse chevron\n clears it. Preview stays owned by the suite (hidden while no tab is\n open). The per-column rules below override the geometry. */\n [data-aionui-explorer-col],\n [data-aionui-preview-col] {\n position: fixed !important;\n z-index: 55 !important;\n background: var(--aion-bg-base, #ffffff) !important;\n border-left: none !important;\n }\n /* Explorer (file tree) bottom sheet: bottom edge aligned exactly with\n the composer card's bottom line \u2014 the card sits 36px above the\n viewport bottom (8px composer padding + the 28px stats strip below\n the card), so the sheet uses the same 36px bottom offset. */\n [data-aionui-explorer-col] {\n visibility: hidden !important;\n left: 8px !important;\n right: 8px !important;\n top: auto !important;\n bottom: 36px !important;\n width: auto !important;\n height: min(55dvh, 460px) !important;\n max-height: calc(100dvh - 44px) !important;\n border-radius: 14px !important;\n overflow: hidden !important;\n box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;\n animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;\n }\n /* Preview (file content) bottom sheet. Gated shut by default: the suite\n persists open preview tabs in localStorage and restores them on load,\n which would pop the sheet over the fresh UI. The client only sets the\n frame marker after the user taps a file row in the explorer; the\n suite's own collapse chevron clears it via the visibility watcher. */\n [data-aionui-preview-col] {\n visibility: hidden !important;\n position: fixed !important;\n left: 8px !important;\n right: 8px !important;\n top: auto !important;\n bottom: 40px !important;\n width: auto !important;\n height: min(50dvh, 420px) !important;\n max-height: calc(100dvh - 48px) !important;\n border-radius: 14px !important;\n overflow: hidden !important;\n box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;\n z-index: 56 !important;\n animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;\n /* Fullscreen toggle (issue #8): animate the geometry change instead of\n snapping. visibility is deliberately not listed, so opening/closing\n the sheet stays instant; the open/close keyframes own transform. */\n transition:\n left .24s var(--ds-ease-out, ease-in-out),\n right .24s var(--ds-ease-out, ease-in-out),\n top .24s var(--ds-ease-out, ease-in-out),\n bottom .24s var(--ds-ease-out, ease-in-out),\n width .24s var(--ds-ease-out, ease-in-out),\n height .24s var(--ds-ease-out, ease-in-out),\n border-radius .24s var(--ds-ease-out, ease-in-out),\n box-shadow .24s var(--ds-ease-out, ease-in-out),\n padding-top .24s var(--ds-ease-out, ease-in-out) !important;\n }\n /* User-opened preview sheet (frame marker, set on file-row tap). */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-preview-col] {\n visibility: visible !important;\n }\n /* The Files action opens the explorer sheet (frame marker). */\n [data-mobile-nav=\"frame\"][data-aionui-explorer-open] [data-aionui-explorer-col] {\n visibility: visible !important;\n }\n /* While the preview sheet is up, the explorer sheet yields (two stacked\n bottom sheets would read as one broken overlay). Closing the preview\n via its collapse chevron / tab close clears the marker, and the\n explorer sheet returns. Same specificity as the explorer-open rule, so\n this must stay AFTER it. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-explorer-col] {\n visibility: hidden !important;\n }\n /* The open drawer must never sit under a sheet: while the frame is in the\n narrow-expanded state both sheets yield (later in the file than the\n open marker rule, so it wins at equal specificity). The fullscreen\n toggle has its own drawer-open rule at the end of its section. */\n [data-mobile-nav=\"frame\"]:not([data-sidebar-collapsed]) [data-aionui-explorer-col],\n [data-mobile-nav=\"frame\"]:not([data-sidebar-collapsed]) [data-aionui-preview-col] {\n visibility: hidden !important;\n display: none !important;\n }\n /* The suite's own expand button reads the store state we bypass on\n mobile \u2014 hide it; the header Files action is the opener. */\n .aionui-floating-expand {\n display: none !important;\n }\n\n /* Preview sheet fullscreen toggle (issue #8): a fixed button parked in the\n sheet's titlebar row, just left of the suite's collapse chevron (24px at\n right:8px of the sheet, and the sheet spans 8px..(100vw-8px)). The top\n calc mirrors the sheet geometry above (bottom 40px + min(50dvh, 420px));\n when the frame carries \"data-mobile-preview-full\" the sheet goes\n fullscreen and the button moves to the viewport corner. */\n [data-mobile-nav=\"preview-full-toggle\"] {\n position: absolute !important;\n right: 36px !important;\n top: 8px !important;\n z-index: 57 !important;\n display: none !important;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--aion-text-secondary, var(--dsw-alias-label-secondary, inherit));\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n /* Native look: same size/radius/hover language as the suite's tab-bar\n icon buttons (the 20px panelCollapse next to it). The button lives\n INSIDE the preview column, so it rides the sheet's own open\n animation and geometry transition \u2014 no curve matching needed. */\n transition: background-color .15s, top .24s var(--ds-ease-out, ease-in-out);\n }\n [data-mobile-nav=\"preview-full-toggle\"]:hover {\n background: var(--aion-bg-3, rgba(0, 0, 0, .22));\n }\n [data-mobile-nav=\"preview-full-toggle\"]:active {\n background: var(--aion-bg-active, rgba(0, 0, 0, .28));\n }\n [data-mobile-nav=\"preview-full-toggle\"]:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);\n outline-offset: 2px;\n }\n [data-mobile-nav=\"preview-full-toggle\"] svg {\n width: 14px;\n height: 14px;\n }\n /* Keep the last tab (and the \"+\" URL-tab trigger) from sliding under the\n fullscreen toggle: reserve the right end of the preview tab row. */\n [data-aionui-preview-col] [class$=\"_tabScroll\"] {\n padding-right: 34px !important;\n }\n /* Visible only while the preview sheet is open. Visibility itself is\n inherited from the column, so the sheet's own hide rules (collapse,\n drawer open) cover the button too. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] {\n display: inline-flex !important;\n }\n /* Icon swap on the frame fullscreen marker. */\n [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-out {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-in {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-out {\n display: inline !important;\n }\n /* Fullscreen preview: the sheet fills the whole viewport (notch included);\n the safe-area padding drops the titlebar row below the status bar, and\n the toggle follows the titlebar into the top corner. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open][data-mobile-preview-full] [data-aionui-preview-col] {\n inset: 0 !important;\n left: 0 !important;\n right: 0 !important;\n top: 0 !important;\n bottom: 0 !important;\n width: 100% !important;\n height: 100dvh !important;\n max-height: none !important;\n box-sizing: border-box !important;\n padding-top: var(--mnav-sat) !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n z-index: 57 !important;\n animation: none !important;\n }\n /* Fullscreen: the column fills the viewport, so the button follows the\n titlebar row down below the notch. */\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] {\n top: calc(var(--mnav-sat) + 8px) !important;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-aionui-preview-col],\n [data-mobile-nav=\"preview-full-toggle\"] {\n transition: none !important;\n animation: none !important;\n }\n }\n\n /* dsh-web-ui sidebar entries (task board / ssh) sit flush against each\n other \u2014 give the injected rows breathing room. */\n button[data-dsh-taskboard-entry],\n button[data-dsh-ssh-entry] {\n margin-bottom: 8px !important;\n }\n\n /* Task board: five kanban columns at minmax(0,1fr) crush into ~78px phone\n strips. Give every column a usable minimum and let the row scroll. */\n [data-dsh-taskboard-board] > [class$=\"_columns\"] {\n grid-template-columns: repeat(5, minmax(240px, 1fr)) !important;\n overflow-x: auto !important;\n }\n /* The floating button must not float over a takeover panel (task board /\n ssh own the center column while active). */\n html[data-dsh-taskboard-active] [data-mobile-nav=\"fab\"],\n html[data-dsh-ssh-active] [data-mobile-nav=\"fab\"],\n html[data-dsh-taskboard-active] [data-mobile-nav=\"backdrop\"],\n html[data-dsh-ssh-active] [data-mobile-nav=\"backdrop\"] {\n display: none !important;\n }\n /* Board header: let the search field take the slack instead of squeezing\n the action buttons. */\n [data-dsh-taskboard-board] > [class$=\"_boardHeader\"] [class$=\"_search\"] {\n flex: 1 1 auto !important;\n min-width: 80px !important;\n }\n\n /* ---------- dsh-web-ui polish: plugin market search ----------\n The market tab row (Discover / Themes / Installed + the plugin search\n box) is a no-wrap flex: at 390px the tabs plus the ~218px search box\n (~475px total) overflow the ~334px sheet and the search box runs off\n the right edge of the screen (it also forces a horizontal scrollbar on\n the sheet's options area). Let the row wrap: the tabs keep the first\n line and the search box gets its own full-width second line. */\n\n [aria-modal=\"true\"] [class$=\"_tabs\"] {\n flex-wrap: wrap !important;\n row-gap: 8px !important;\n }\n [aria-modal=\"true\"] [class$=\"_searchInline\"] {\n flex: 1 1 100% !important;\n width: 100% !important;\n max-width: 100% !important;\n }\n\n /* ---------- dsh-usage-stats polish: usage & balance panel ----------\n The panel's stats row shows three token counters side by side\n (today / month / total). The counters use tabular nowrap figures whose\n min-content width overflows the ~336px panel body on a phone: figures\n clip at the row's edges and the panel grows a horizontal scrollbar.\n Stack the three counters vertically \u2014 full-width rows, so the figures\n always fit. */\n\n [class*=\"usg_\"][class$=\"_statsRow\"] {\n flex-direction: column !important;\n }\n [class*=\"usg_\"][class$=\"_stat\"] {\n flex: 0 0 auto !important;\n width: 100% !important;\n min-width: 0 !important;\n }\n\n /* ---------- dsh-web-ui polish: settings sheet ----------\n The official dialog is a desktop two-column form; on a phone the\n label/control split leaves a huge dead gap and long descriptions wrap\n into tall stacks. Stack each row (text above, control full-width) and\n keep the nav tabs on ONE horizontally scrolling row. */\n\n /* Nav tabs: single scrolling row instead of the 3-per-row grid \u2014 seven\n categories wrap into three rows on a phone (~130px of sheet height);\n one row with a thin scrollbar keeps every tab reachable and returns\n that space to the options area (user feedback 2026-08-16). An earlier\n one-row attempt had no scroll affordance and silently cut the last\n tab off; the thin scrollbar IS the affordance. Scoped to the frame\n marker: the desktop dialog keeps its official vertical nav column. */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"]:has(> :first-child > :last-child > button):not(:has([role=\"navigation\"])):not(:has([class*=\"ZuhsRW\"])) > :first-child [class$=\"_navList\"] {\n display: flex !important;\n flex-wrap: nowrap !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n gap: 6px !important;\n width: 100% !important;\n scrollbar-width: thin !important;\n -webkit-overflow-scrolling: touch !important;\n }\n /* Hairline scrollbar for the tab row: the default WebKit scrollbar reads\n fat on a phone; 2px keeps the scroll affordance without the bulk. */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar {\n height: 2px !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .22)) !important;\n border-radius: 1px !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar-track {\n background: transparent !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navCell\"] {\n flex: 0 0 auto !important;\n white-space: nowrap !important;\n padding: 6px 8px !important;\n gap: 6px !important;\n font-size: 13px !important;\n justify-content: flex-start !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navCell\"] svg {\n width: 14px !important;\n height: 14px !important;\n flex: none !important;\n }\n /* Content toolbar: the \"Open configuration file\" button is hidden on\n mobile \u2014 it is rarely needed on a phone and steals ~180px from the\n tab row's scroll area (user feedback 2026-08-16). Only the close \u2715\n stays, flush right in the nav row. Desktop untouched (frame scoped). */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_header\"] [class$=\"_actions\"] {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_header\"] [class$=\"_actions\"] [class$=\"_action\"] {\n font-size: 13px !important;\n padding: 6px 12px !important;\n min-height: 0 !important;\n }\n /* Setting rows: text on top, control below at full width. */\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] {\n flex-direction: column !important;\n align-items: stretch !important;\n gap: 8px !important;\n }\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] > :first-child {\n width: 100% !important;\n max-width: none !important;\n }\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] > :last-child {\n width: 100% !important;\n max-width: none !important;\n }\n /* Appearance mode group: give the cube row a consistent bordered\n segmented look (the official borders differ per state). */\n [aria-modal=\"true\"] [class$=\"_cubeRow\"] > * {\n border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;\n }\n\n /* ---------- dsh-web-ui polish: explorer sheet ----------\n The aionui explorer was designed for a desktop side column: compact the\n header, search box and tree rows so a phone shows more entries, and pad\n the scroll bottom so the last row never sits flush on the edge. */\n\n [data-aionui-explorer-col] [class$=\"_tabBar\"] {\n height: 36px !important;\n }\n [data-aionui-explorer-col] [class$=\"_tabBtn\"],\n [data-aionui-explorer-col] [class$=\"_tabBtnActive\"] {\n padding: 0 12px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class$=\"_searchBox\"] {\n height: 32px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class*=\"_treeRow\"] {\n height: 30px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class*=\"_treeRow\"] svg {\n width: 14px !important;\n height: 14px !important;\n }\n [data-aionui-explorer-col] [class$=\"_scrollArea\"] {\n padding-bottom: 28px !important;\n }\n\n /* ---------- dsh-web-ui polish: drawer footer ----------\n The injected footer actions (Files + Session log) become two equal pill\n buttons instead of text-width capsules. */\n\n /* The official footerActions row also hosts the remote-web-ui entry\n row (two icon buttons); without wrapping the two groups squeeze each\n other on one line. Wrap so each group gets its own full-width row. */\n [data-mobile-nav=\"frame\"] [class$=\"_footerActions\"] {\n flex-wrap: wrap !important;\n gap: 6px !important;\n }\n [data-mobile-nav=\"drawer-actions\"] {\n width: 100% !important;\n }\n [data-mobile-nav=\"drawer-actions\"] > button {\n flex: 1 1 0 !important;\n padding: 0 8px !important;\n white-space: nowrap !important;\n }\n\n /* ---------- dsh-web-ui polish: floating pet ----------\n The whale-girl pet (dsh-pet) floats at the viewport corner with a\n persisted, draggable position. On phones the pet is scaled down so\n it does not dominate the screen; the plugin's own drag + persist\n still work (the position itself is left alone \u2014 the mobile default\n position is seeded via the pet API to just above the composer). */\n\n body > [class$=\"_float\"]:has([class$=\"_sprite\"][role=\"button\"]) {\n transform: scale(.66);\n transform-origin: bottom right;\n }\n /* While a modal dialog (settings sheet / export) owns the screen the pet\n floats ABOVE it and covers the dialog content; modal semantics say the\n background is inert, so hide the pet for the modal's lifetime. */\n body:has([aria-modal=\"true\"]) > [class$=\"_float\"]:has([class$=\"_sprite\"][role=\"button\"]) {\n display: none !important;\n }\n\n /* ---------- conversation stats line ----------\n The official session-status row (turns / steps / LLM time / TTFT /\n cache) is long. It is the single entry the official StatsLine puts in\n conversation.composer.dock, so the structural anchor below reaches it\n without any DOM marking (S3 deleted the text-matching effect that used\n to set [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]). Layout: ONE fixed-height (28px) flex\n strip that scrolls horizontally \u2014 the full metrics stream stays\n reachable by swiping, the row never grows vertically, no ellipsis or\n fade, 12px gaps between metric groups, a 2px scrollbar as the swipe\n affordance. The phone breakpoint hides the strip outright instead\n (styles/composer.css.ts \u2014 its data moves into the session info card). */\n\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n width: 100% !important;\n max-width: 100% !important;\n min-width: 0 !important;\n height: 28px !important;\n min-height: 28px !important;\n max-height: 28px !important;\n box-sizing: border-box !important;\n white-space: nowrap !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior-x: contain;\n scrollbar-width: thin !important;\n scrollbar-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .28)) transparent !important;\n padding: 0 0 4px !important;\n line-height: 20px !important;\n font-size: 12px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar {\n height: 2px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .3)) !important;\n border-radius: 2px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar-track {\n background: transparent !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] > * {\n display: flex !important;\n flex: 0 0 auto !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n width: max-content !important;\n min-width: max-content !important;\n max-width: none !important;\n white-space: nowrap !important;\n margin-right: 12px !important;\n padding: 0 !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] > *:last-child {\n margin-right: 0 !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] * {\n white-space: nowrap !important;\n }\n\n /* ---------- dsh-genui panel dock ----------\n The genui panel docks above the composer (conversation.input.dock,\n id genui-panel). On a phone its business-blue outline, generous chrome\n and single-line ellipsis read as an unfinished artifact: long titles\n truncate mid-word (\"\u2026default b\u00B7\u00B7\u00B7\") with the chevron glued to the\n ellipsis, and the pill crowds the composer. Mobile treatment: neutral\n card border matching the composer, tighter chrome so the full title\n fits, chevron with breathing room. Scoped to the mobile frame marker \u2014\n desktop keeps genui's own styling untouched. */\n\n [data-mobile-nav=\"frame\"] [data-genui-panel] {\n margin: 6px 12px 4px !important;\n border-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;\n border-radius: 12px !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelToggle\"] {\n padding: 7px 12px !important;\n gap: 8px !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelBadge\"] {\n padding: 0 7px !important;\n border-radius: 5px !important;\n font-size: 10.5px !important;\n line-height: 1.7 !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelTitle\"] {\n flex: 1 1 auto !important;\n min-width: 0 !important;\n font-size: 12.5px !important;\n line-height: 1.45 !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelChevron\"] {\n flex: none !important;\n margin-left: 0 !important;\n padding-left: 4px !important;\n }\n\n /* ---------- git-graph branch chip: inside the composer card ----------\n The branch chip (conversation.input.dock) floats between the dock rows\n and the input card; on a phone it reads as a stray capsule crowding the\n composer. A client effect (MobileNavOverlay) reparents the chip INTO\n the composer card; these rules pin it to the card's top-left and give\n the card a dedicated chip row. The card is position: relative by the\n official stylesheet, so the absolute anchor resolves against it. The\n plugin's own sheet sets all four offsets on the anchor, so right/bottom\n must be neutralized too. Scope is the frame marker + the anchor\n attribute (NOT the dock slot \u2014 the reparenting moves the chip out of\n the dock's subtree). Desktop untouched: the frame marker only exists\n below 1024px, and the effect restores the chip to the dock when the\n viewport widens. Chip row geometry (2026-08-16, user feedback): 48px\n padding left a 16px dead gap between the chip and the input line and\n made the composer read too tall; the row is now 40px = chip (24px) at\n top 12px + ~4px to the textarea \u2014 the chip sits slightly lower and\n the gap is compressed without touching the official height budget\n further. */\n\n [data-mobile-nav=\"frame\"] [data-gitgraph-chip-anchor] {\n position: absolute !important;\n top: 12px !important;\n left: 12px !important;\n right: auto !important;\n bottom: auto !important;\n z-index: 1 !important;\n }\n [data-mobile-nav=\"frame\"] [class$=\"_card\"]:has([data-gitgraph-chip-anchor]) {\n padding-top: 40px !important;\n }\n\n /* ---------- dsh-better-sidebar: safe area (S2.1, 2026-08-17) ----------\n THIRD-PARTY COMPAT RULE \u2014 dsh-better-sidebar (the workbench the session\n header's panel button opens). Its shell is viewport-fixed and starts at\n y=0: the panel at inset 0 (100vw drawer below 768px, a right column\n above it) and the toggle cluster at top:3px. Neither knows about\n env(safe-area-inset-*), so on a notched iPhone the whole tab strip \u2014\n including the one button that CLOSES the panel \u2014 sits behind the status\n bar and cannot be tapped: the user opened the workbench and was stuck\n there (real-device report, 2026-08-17).\n Applied across the plugin's whole mobile band, not just <768px: the\n panel is fixed at top:0 in the 768-1023px range too, so the same notch\n covers the same tab strip. Zero effect wherever the inset is 0 (every\n desktop browser, every non-notched device) \u2014 desktop is >=1024px and\n out of this media block entirely.\n Anchors: the plugin's own mount marker [data-dsh-better-sidebar]\n (index.tsx) plus class-suffix selectors, per this repo's hashed-class\n convention. --dsh-title-bar-strip is the plugin's own title-bar-compat\n offset (set only while that mode is on, 0px fallback otherwise): adding\n to it keeps both offsets rather than clobbering theirs.\n !important because their :global(body[...]) rules outrank a plain\n attribute selector.\n No box-sizing here on purpose: the panel is position:fixed with BOTH\n top and bottom set, so its used height already resolves to\n \"containing block - insets - padding - border\" (CSS 2.1 10.6.4) and the\n padding shrinks the content box without any help. Forcing border-box\n also folds their 1px left border into the inline width \u2014 measured as a\n 1px panel-width change at 768px, i.e. a regression outside this\n hotfix's remit. */\n [data-dsh-better-sidebar] [class$=\"_panel\"],\n [data-dsh-better-sidebar] [class$=\"_panelHidden\"] {\n padding-top: calc(var(--mnav-sat) + var(--dsh-title-bar-strip, 0px)) !important;\n }\n /* Only the tablet/desktop range still shows the cluster (see the hide\n rule below) \u2014 the notch offset is now dead weight below 768px, so it\n is scoped out rather than left applying invisibly. min-width:768px\n rather than the more common max-width pairing: this offset was\n already a harmless no-op at >=1024px before S3.1 (--mnav-sat is 0 on\n every desktop browser), so narrowing its floor to 768px changes\n nothing there either \u2014 it only stops evaluating on phone. */\n @media (min-width: 768px) {\n [data-dsh-better-sidebar] [class$=\"_toggleCluster\"] {\n top: calc(var(--mnav-sat) + var(--dsh-title-bar-strip, 0px) + 3px) !important;\n }\n }\n\n /* ---------- dsh-better-sidebar: hide the phone toggle cluster (S3.1, 2026-08-17) ----------\n Real-device round 2 feedback: this fixed top-right cluster duplicates\n \u2014 and visually overlaps \u2014 the session header's own workbench button\n (MobileSessionHeader.tsx), which already opens/closes the same panel\n by clicking this cluster's toggle button through. Hidden below 768px\n only; the 768-1023px tablet range has no workbench button (header.css.ts\n scopes that entire reflow to <768px) and still depends on this cluster\n as its only entry point, so it stays exactly as v1.0.0/S2.1 shipped it\n there. The cluster also holds the panel's own close affordance \u2014 see\n [data-mobile-nav=\"better-sidebar-close\"] below for the phone\n replacement, wired up in MobileSessionHeader.tsx. */\n @media (max-width: 767px) {\n [data-dsh-better-sidebar] [class$=\"_toggleCluster\"] {\n display: none !important;\n }\n }\n\n /* ---------- dsh-better-sidebar: phone close button (S3.1 follow-up, 2026-08-17) ----------\n Appended to document.body by MobileSessionHeader.tsx's\n MobileHeaderUtilities effect \u2014 never inside the panel's own subtree\n (the third party's React re-renders would wipe it) and never under any\n transformed/backdrop-filter ancestor (the S4 info-card WebKit lesson in\n AGENTS.md: position:fixed re-anchors to the nearest such ancestor\n instead of the viewport). Default hidden \u2014 belt-and-braces, same\n reasoning as header.css.ts's [data-mobile-nav=\"header-info\"] etc list:\n React does not know about media queries. Shown only below 768px AND\n only while the panel is actually open: the panel's own class name ends\n in \"_panel\" exclusively in the open state (the \"_panelHidden\" suffix is\n appended once closed, so the string no longer ends in \"_panel\") \u2014 a\n pure-CSS :has() open/closed read, no MutationObserver required.\n\n Bottom-center pill, not a top-right circle (real-device follow-up,\n 2026-08-17): a top-right position collided with the panel's own\n per-tab toolbar controls \u2014 measured live at 390px with the explorer\n tab open, the panel's Refresh button sits at x:354-382 y:93-121, and a\n 44px circle at top:8px+safe-area/right:8px lands at x:338-382\n y:(safe-area+8)-(safe-area+52), a direct overlap once the safe-area\n offset is small (or zero on non-notched phones). Every per-tab toolbar\n (explorer/git/tabBar) lives at the panel's TOP; nothing in the\n default explorer or git tabs reaches the bottom 90px of the viewport\n (checked live, both tabs, 2026-08-17), so bottom-center is clear\n regardless of which tab is open \u2014 one fixed position that does not\n need per-tab-type coordinates to dodge. */\n /* Default-hidden for this pill lives in header.css.ts, NOT here: this\n whole file sits inside the shared (max-width: 1023px) block, so a rule\n here can never hide anything at desktop widths (learned the hard way,\n 2026-08-17 desktop leak). */\n @media (max-width: 767px) {\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"] {\n display: flex !important;\n position: fixed;\n left: 50%;\n bottom: calc(var(--mnav-sab) + 12px);\n transform: translateX(-50%);\n z-index: 70;\n align-items: center;\n gap: 6px;\n height: 44px;\n padding: 0 18px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-bg-base, #fff);\n /* Drop shadow alone does not draw an edge: the fill is bg-base, which in\n dark theme is the same colour as the surface behind it, and a black\n shadow on a dark ground is invisible \u2014 the pill read as floating text\n with no boundary. The inset hairline is the edge; the drop shadow only\n lifts it. Same token the session rows use for their dark-mode ring. */\n box-shadow:\n inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, .14)),\n 0 4px 16px rgba(0, 0, 0, .2);\n color: var(--dsw-alias-label-primary, inherit);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"] svg {\n width: 14px;\n height: 14px;\n flex: none;\n }\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n }\n\n /* ---------- dsh-agent-teams: phone floater reposition (2026-08-18) ----------\n THIRD-PARTY COMPAT RULE \u2014 @nanmicoder/dsh-agent-teams (the AgentTeams\n activity floater: a collapsed badge + expandable panel, body-portaled\n under its own mount marker div[data-agent-teams-host]). Its own CSS\n fixes both at top:56px (<=640px) viewport coordinates and never reads\n env(safe-area-inset-*), so on a notched iPhone the badge lands INSIDE\n the safe-area + this plugin's 76px session header band \u2014 overlapping\n the info and workbench buttons (real-device report, 2026-08-18).\n Move both below the header instead: safe-area + 76px header chrome\n (48px title row + 28px view-switch row, header.css.ts) + 8px gap.\n Anchored on the plugin's own mount marker + class-suffix selectors per\n this repo's hashed-class convention \u2014 when dsh-agent-teams is not\n installed the marker never exists and this whole section is inert\n (same presence-gating as the dsh-better-sidebar rules above).\n !important because their positions come from html-level CSS vars\n (--agent-teams-panel-top et al.) whose style tag order vs ours is not\n guaranteed. z-index: their 2147483000 would float the badge over every\n one of this plugin's own overlays (drawer 40, info sheet 55-57, close\n pill 70); 30 keeps it above content but below all of them. */\n @media (max-width: 767px) {\n [data-agent-teams-host] [class$=\"_badge\"],\n [data-agent-teams-host] [class$=\"_panel\"] {\n top: calc(var(--mnav-sat) + 84px) !important;\n right: 10px !important;\n z-index: 30 !important;\n }\n [data-agent-teams-host] [class$=\"_panel\"] {\n left: 10px !important;\n width: auto !important;\n /* Their min-height:min(560px, calc(100dvh - 56 - 56)) resolves against\n the OLD top var \u2014 on short phones it exceeds the space left below\n the moved-down top edge and would force overflow; content-sized with\n a hard cap replaces it. 76px bottom clearance keeps the collapsed\n composer's top edge visible under the panel. */\n min-height: 0 !important;\n max-height: calc(100dvh - var(--mnav-sat) - 84px - var(--mnav-sab) - 76px) !important;\n }\n /* The floater belongs to the SESSION page: on the home list it hovered\n over the session rows (user feedback, 2026-08-18). The phone page\n stack is this plugin's own overlay \u2014 the official conversation (and\n with it the floater's \"current session\") stays mounted underneath, so\n agent-teams keeps rendering it; hide it whenever the home level is the\n visible one (data-view flips to \"session\" inside a session, and the\n badge/panel come right back). :has() is already a documented\n requirement of this plugin (Chromium 105+, docs/interface.md). */\n body:has([data-mobile-nav=\"home\"][data-view=\"home\"]) [data-agent-teams-host] [class$=\"_badge\"],\n body:has([data-mobile-nav=\"home\"][data-view=\"home\"]) [data-agent-teams-host] [class$=\"_panel\"] {\n display: none !important;\n }\n }\n\n /* ---------- dsh-at-file compatibility ----------\n dsh-at-file registers its own `conversation.input.dock` entry\n ([data-at-file-dock]) and renders one row per `@path` token in the\n draft. That is exactly the same source this plugin's attachment chips\n read, so on a phone a single uploaded image produced TWO entries on the\n composer rail: our 48px thumbnail tile AND at-file's file-name row for\n the same token (reported 2026-08-22).\n\n Drop at-file's row for the upload directory only. Everything else the\n user @-mentions by hand is at-file's own job and stays \u2014 this is not a\n blanket hide. The path is readable from the row's own title attribute\n (FilesDock passes `title={mention.relative}`), which is a rendered\n contract rather than a hashed class name.\n\n Phone only, and deliberately so: at >= 768px our chips are hidden by\n composer.css.ts, which leaves at-file's row as the ONLY thing on screen\n representing the attachment. Hiding it there would make an uploaded file\n invisible.\n\n When every row was an upload the container itself is left empty, and an\n empty flex item still claims the rail's 6px gap \u2014 so the dock goes too\n unless at least one non-upload row survives. */\n @media (max-width: 767px) {\n [data-at-file-dock] [data-at-file-row]:has([title^=\".dsh-uploads/\"]) {\n display: none !important;\n }\n [data-at-file-dock]:not(:has([data-at-file-row] [title]:not([title^=\".dsh-uploads/\"]))) {\n display: none !important;\n }\n }\n";
1
+ export declare const COMPAT_CSS = " /* ---------- dsh-web-ui family compatibility ----------\n The linxin666 plugin suite extends the shell frame directly:\n - aionui-panel appends two trailing grid columns (explorer / preview)\n plus absolute drag handles to [data-dsh-frame]; its 5-track inline\n grid is already overridden above, but the handles and columns would\n still float over the main UI. On mobile the columns leave the grid\n as floating bottom sheets and keep their own visibility state \u2014\n the suite's collapse chevron / preview tabs still work, so no\n feature is lost. The task-board / ssh plugins inject sidebar\n entries and center-column takeover panels; the entries need\n spacing and the kanban needs scrollable columns. */\n\n /* Touch devices: the drag handles are useless \u2014 the floating expand\n button is the opener. */\n .aionui-explorer-handle,\n .aionui-preview-handle {\n display: none !important;\n }\n\n /* Shared base: both columns leave the grid as floating panels. The\n explorer is gated shut by default (its own persisted expanded state\n must never cover the mobile UI on load); the header Files action opens\n it via the frame marker below, and the sheet's own collapse chevron\n clears it. Preview stays owned by the suite (hidden while no tab is\n open). The per-column rules below override the geometry. */\n [data-aionui-explorer-col],\n [data-aionui-preview-col] {\n position: fixed !important;\n z-index: 55 !important;\n background: var(--aion-bg-base, #ffffff) !important;\n border-left: none !important;\n }\n /* Explorer (file tree) bottom sheet: bottom edge aligned exactly with\n the composer card's bottom line \u2014 the card sits 36px above the\n viewport bottom (8px composer padding + the 28px stats strip below\n the card), so the sheet uses the same 36px bottom offset. */\n [data-aionui-explorer-col] {\n visibility: hidden !important;\n left: 8px !important;\n right: 8px !important;\n top: auto !important;\n bottom: 36px !important;\n width: auto !important;\n height: min(55dvh, 460px) !important;\n max-height: calc(100dvh - 44px) !important;\n border-radius: 14px !important;\n overflow: hidden !important;\n box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;\n animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;\n }\n /* Preview (file content) bottom sheet. Gated shut by default: the suite\n persists open preview tabs in localStorage and restores them on load,\n which would pop the sheet over the fresh UI. The client only sets the\n frame marker after the user taps a file row in the explorer; the\n suite's own collapse chevron clears it via the visibility watcher. */\n [data-aionui-preview-col] {\n visibility: hidden !important;\n position: fixed !important;\n left: 8px !important;\n right: 8px !important;\n top: auto !important;\n bottom: 40px !important;\n width: auto !important;\n height: min(50dvh, 420px) !important;\n max-height: calc(100dvh - 48px) !important;\n border-radius: 14px !important;\n overflow: hidden !important;\n box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;\n z-index: 56 !important;\n animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;\n /* Fullscreen toggle (issue #8): animate the geometry change instead of\n snapping. visibility is deliberately not listed, so opening/closing\n the sheet stays instant; the open/close keyframes own transform. */\n transition:\n left .24s var(--ds-ease-out, ease-in-out),\n right .24s var(--ds-ease-out, ease-in-out),\n top .24s var(--ds-ease-out, ease-in-out),\n bottom .24s var(--ds-ease-out, ease-in-out),\n width .24s var(--ds-ease-out, ease-in-out),\n height .24s var(--ds-ease-out, ease-in-out),\n border-radius .24s var(--ds-ease-out, ease-in-out),\n box-shadow .24s var(--ds-ease-out, ease-in-out),\n padding-top .24s var(--ds-ease-out, ease-in-out) !important;\n }\n /* User-opened preview sheet (frame marker, set on file-row tap). */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-preview-col] {\n visibility: visible !important;\n }\n /* The Files action opens the explorer sheet (frame marker). */\n [data-mobile-nav=\"frame\"][data-aionui-explorer-open] [data-aionui-explorer-col] {\n visibility: visible !important;\n }\n /* While the preview sheet is up, the explorer sheet yields (two stacked\n bottom sheets would read as one broken overlay). Closing the preview\n via its collapse chevron / tab close clears the marker, and the\n explorer sheet returns. Same specificity as the explorer-open rule, so\n this must stay AFTER it. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-explorer-col] {\n visibility: hidden !important;\n }\n /* The open drawer must never sit under a sheet: while the frame is in the\n narrow-expanded state both sheets yield (later in the file than the\n open marker rule, so it wins at equal specificity). The fullscreen\n toggle has its own drawer-open rule at the end of its section. */\n [data-mobile-nav=\"frame\"]:not([data-sidebar-collapsed]) [data-aionui-explorer-col],\n [data-mobile-nav=\"frame\"]:not([data-sidebar-collapsed]) [data-aionui-preview-col] {\n visibility: hidden !important;\n display: none !important;\n }\n /* The suite's own expand button reads the store state we bypass on\n mobile \u2014 hide it; the header Files action is the opener. */\n .aionui-floating-expand {\n display: none !important;\n }\n\n /* Preview sheet fullscreen toggle (issue #8): a fixed button parked in the\n sheet's titlebar row, just left of the suite's collapse chevron (24px at\n right:8px of the sheet, and the sheet spans 8px..(100vw-8px)). The top\n calc mirrors the sheet geometry above (bottom 40px + min(50dvh, 420px));\n when the frame carries \"data-mobile-preview-full\" the sheet goes\n fullscreen and the button moves to the viewport corner. */\n [data-mobile-nav=\"preview-full-toggle\"] {\n position: absolute !important;\n right: 36px !important;\n top: 8px !important;\n z-index: 57 !important;\n display: none !important;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n padding: 0;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--aion-text-secondary, var(--dsw-alias-label-secondary, inherit));\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n /* Native look: same size/radius/hover language as the suite's tab-bar\n icon buttons (the 20px panelCollapse next to it). The button lives\n INSIDE the preview column, so it rides the sheet's own open\n animation and geometry transition \u2014 no curve matching needed. */\n transition: background-color .15s, top .24s var(--ds-ease-out, ease-in-out);\n }\n [data-mobile-nav=\"preview-full-toggle\"]:hover {\n background: var(--aion-bg-3, rgba(0, 0, 0, .22));\n }\n [data-mobile-nav=\"preview-full-toggle\"]:active {\n background: var(--aion-bg-active, rgba(0, 0, 0, .28));\n }\n [data-mobile-nav=\"preview-full-toggle\"]:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);\n outline-offset: 2px;\n }\n [data-mobile-nav=\"preview-full-toggle\"] svg {\n width: 14px;\n height: 14px;\n }\n /* Keep the last tab (and the \"+\" URL-tab trigger) from sliding under the\n fullscreen toggle: reserve the right end of the preview tab row. */\n [data-aionui-preview-col] [class$=\"_tabScroll\"] {\n padding-right: 34px !important;\n }\n /* Visible only while the preview sheet is open. Visibility itself is\n inherited from the column, so the sheet's own hide rules (collapse,\n drawer open) cover the button too. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] {\n display: inline-flex !important;\n }\n /* Icon swap on the frame fullscreen marker. */\n [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-out {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-in {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] .dsh-mobile-nav-full-out {\n display: inline !important;\n }\n /* Fullscreen preview: the sheet fills the whole viewport (notch included);\n the safe-area padding drops the titlebar row below the status bar, and\n the toggle follows the titlebar into the top corner. */\n [data-mobile-nav=\"frame\"][data-aionui-preview-open][data-mobile-preview-full] [data-aionui-preview-col] {\n inset: 0 !important;\n left: 0 !important;\n right: 0 !important;\n top: 0 !important;\n bottom: 0 !important;\n width: 100% !important;\n height: 100dvh !important;\n max-height: none !important;\n box-sizing: border-box !important;\n padding-top: var(--mnav-sat) !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n z-index: 57 !important;\n animation: none !important;\n }\n /* Fullscreen: the column fills the viewport, so the button follows the\n titlebar row down below the notch. */\n [data-mobile-nav=\"frame\"][data-mobile-preview-full] [data-aionui-preview-col] [data-mobile-nav=\"preview-full-toggle\"] {\n top: calc(var(--mnav-sat) + 8px) !important;\n }\n @media (prefers-reduced-motion: reduce) {\n [data-aionui-preview-col],\n [data-mobile-nav=\"preview-full-toggle\"] {\n transition: none !important;\n animation: none !important;\n }\n }\n\n /* dsh-web-ui sidebar entries (task board / ssh) sit flush against each\n other \u2014 give the injected rows breathing room. */\n button[data-dsh-taskboard-entry],\n button[data-dsh-ssh-entry] {\n margin-bottom: 8px !important;\n }\n\n /* Task board: five kanban columns at minmax(0,1fr) crush into ~78px phone\n strips. Give every column a usable minimum and let the row scroll. */\n [data-dsh-taskboard-board] > [class$=\"_columns\"] {\n grid-template-columns: repeat(5, minmax(240px, 1fr)) !important;\n overflow-x: auto !important;\n }\n /* The floating button must not float over a takeover panel (task board /\n ssh own the center column while active). */\n html[data-dsh-taskboard-active] [data-mobile-nav=\"fab\"],\n html[data-dsh-ssh-active] [data-mobile-nav=\"fab\"],\n html[data-dsh-taskboard-active] [data-mobile-nav=\"backdrop\"],\n html[data-dsh-ssh-active] [data-mobile-nav=\"backdrop\"] {\n display: none !important;\n }\n /* Board header: let the search field take the slack instead of squeezing\n the action buttons. */\n [data-dsh-taskboard-board] > [class$=\"_boardHeader\"] [class$=\"_search\"] {\n flex: 1 1 auto !important;\n min-width: 80px !important;\n }\n\n /* ---------- dsh-web-ui polish: plugin market search ----------\n The market tab row (Discover / Themes / Installed + the plugin search\n box) is a no-wrap flex: at 390px the tabs plus the ~218px search box\n (~475px total) overflow the ~334px sheet and the search box runs off\n the right edge of the screen (it also forces a horizontal scrollbar on\n the sheet's options area). Let the row wrap: the tabs keep the first\n line and the search box gets its own full-width second line. */\n\n [aria-modal=\"true\"] [class$=\"_tabs\"] {\n flex-wrap: wrap !important;\n row-gap: 8px !important;\n }\n [aria-modal=\"true\"] [class$=\"_searchInline\"] {\n flex: 1 1 100% !important;\n width: 100% !important;\n max-width: 100% !important;\n }\n\n /* ---------- dsh-usage-stats polish: usage & balance panel ----------\n The panel's stats row shows three token counters side by side\n (today / month / total). The counters use tabular nowrap figures whose\n min-content width overflows the ~336px panel body on a phone: figures\n clip at the row's edges and the panel grows a horizontal scrollbar.\n Stack the three counters vertically \u2014 full-width rows, so the figures\n always fit. */\n\n [class*=\"usg_\"][class$=\"_statsRow\"] {\n flex-direction: column !important;\n }\n [class*=\"usg_\"][class$=\"_stat\"] {\n flex: 0 0 auto !important;\n width: 100% !important;\n min-width: 0 !important;\n }\n\n /* ---------- dsh-web-ui polish: settings sheet ----------\n The official dialog is a desktop two-column form; on a phone the\n label/control split leaves a huge dead gap and long descriptions wrap\n into tall stacks. Stack each row (text above, control full-width) and\n keep the nav tabs on ONE horizontally scrolling row. */\n\n /* Nav tabs: single scrolling row instead of the 3-per-row grid \u2014 seven\n categories wrap into three rows on a phone (~130px of sheet height);\n one row with a thin scrollbar keeps every tab reachable and returns\n that space to the options area (user feedback 2026-08-16). An earlier\n one-row attempt had no scroll affordance and silently cut the last\n tab off; the thin scrollbar IS the affordance. Scoped to the frame\n marker: the desktop dialog keeps its official vertical nav column. */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"]:has(> :first-child > :last-child > button):not(:has([role=\"navigation\"])):not(:has([class*=\"ZuhsRW\"])) > :first-child [class$=\"_navList\"] {\n display: flex !important;\n flex-wrap: nowrap !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n gap: 6px !important;\n width: 100% !important;\n scrollbar-width: thin !important;\n -webkit-overflow-scrolling: touch !important;\n }\n /* Hairline scrollbar for the tab row: the default WebKit scrollbar reads\n fat on a phone; 2px keeps the scroll affordance without the bulk. */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar {\n height: 2px !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-border-l2, rgba(0, 0, 0, .22)) !important;\n border-radius: 1px !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navList\"]::-webkit-scrollbar-track {\n background: transparent !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navCell\"] {\n flex: 0 0 auto !important;\n white-space: nowrap !important;\n padding: 6px 8px !important;\n gap: 6px !important;\n font-size: 13px !important;\n justify-content: flex-start !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_navCell\"] svg {\n width: 14px !important;\n height: 14px !important;\n flex: none !important;\n }\n /* Content toolbar: the \"Open configuration file\" button is hidden on\n mobile \u2014 it is rarely needed on a phone and steals ~180px from the\n tab row's scroll area (user feedback 2026-08-16). Only the close \u2715\n stays, flush right in the nav row. Desktop untouched (frame scoped). */\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_header\"] [class$=\"_actions\"] {\n display: none !important;\n }\n [data-mobile-nav=\"frame\"] [aria-modal=\"true\"] [class$=\"_header\"] [class$=\"_actions\"] [class$=\"_action\"] {\n font-size: 13px !important;\n padding: 6px 12px !important;\n min-height: 0 !important;\n }\n /* Setting rows: text on top, control below at full width. */\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] {\n flex-direction: column !important;\n align-items: stretch !important;\n gap: 8px !important;\n }\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] > :first-child {\n width: 100% !important;\n max-width: none !important;\n }\n [aria-modal=\"true\"] [class$=\"_section\"] [class$=\"_row\"] > :last-child {\n width: 100% !important;\n max-width: none !important;\n }\n /* Appearance mode group: give the cube row a consistent bordered\n segmented look (the official borders differ per state). */\n [aria-modal=\"true\"] [class$=\"_cubeRow\"] > * {\n border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;\n }\n\n /* ---------- dsh-web-ui polish: explorer sheet ----------\n The aionui explorer was designed for a desktop side column: compact the\n header, search box and tree rows so a phone shows more entries, and pad\n the scroll bottom so the last row never sits flush on the edge. */\n\n [data-aionui-explorer-col] [class$=\"_tabBar\"] {\n height: 36px !important;\n }\n [data-aionui-explorer-col] [class$=\"_tabBtn\"],\n [data-aionui-explorer-col] [class$=\"_tabBtnActive\"] {\n padding: 0 12px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class$=\"_searchBox\"] {\n height: 32px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class*=\"_treeRow\"] {\n height: 30px !important;\n font-size: 13px !important;\n }\n [data-aionui-explorer-col] [class*=\"_treeRow\"] svg {\n width: 14px !important;\n height: 14px !important;\n }\n [data-aionui-explorer-col] [class$=\"_scrollArea\"] {\n padding-bottom: 28px !important;\n }\n\n /* ---------- dsh-web-ui polish: drawer footer ----------\n The injected footer actions (Files + Session log) become two equal pill\n buttons instead of text-width capsules. */\n\n /* The official footerActions row also hosts the remote-web-ui entry\n row (two icon buttons); without wrapping the two groups squeeze each\n other on one line. Wrap so each group gets its own full-width row. */\n [data-mobile-nav=\"frame\"] [class$=\"_footerActions\"] {\n flex-wrap: wrap !important;\n gap: 6px !important;\n }\n [data-mobile-nav=\"drawer-actions\"] {\n width: 100% !important;\n }\n [data-mobile-nav=\"drawer-actions\"] > button {\n flex: 1 1 0 !important;\n padding: 0 8px !important;\n white-space: nowrap !important;\n }\n\n /* ---------- dsh-web-ui polish: floating pet ----------\n The whale-girl pet (dsh-pet) floats at the viewport corner with a\n persisted, draggable position. On phones the pet is scaled down so\n it does not dominate the screen; the plugin's own drag + persist\n still work (the position itself is left alone \u2014 the mobile default\n position is seeded via the pet API to just above the composer). */\n\n body > [class$=\"_float\"]:has([class$=\"_sprite\"][role=\"button\"]) {\n transform: scale(.66);\n transform-origin: bottom right;\n }\n /* While a modal dialog (settings sheet / export) owns the screen the pet\n floats ABOVE it and covers the dialog content; modal semantics say the\n background is inert, so hide the pet for the modal's lifetime. */\n body:has([aria-modal=\"true\"]) > [class$=\"_float\"]:has([class$=\"_sprite\"][role=\"button\"]) {\n display: none !important;\n }\n\n /* ---------- conversation stats line ----------\n The official session-status row (turns / steps / LLM time / TTFT /\n cache) is long. It is the single entry the official StatsLine puts in\n conversation.composer.dock, so the structural anchor below reaches it\n without any DOM marking (S3 deleted the text-matching effect that used\n to set [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]). Layout: ONE fixed-height (28px) flex\n strip that scrolls horizontally \u2014 the full metrics stream stays\n reachable by swiping, the row never grows vertically, no ellipsis or\n fade, 12px gaps between metric groups, a 2px scrollbar as the swipe\n affordance. The phone breakpoint hides the strip outright instead\n (styles/composer.css.ts \u2014 its data moves into the session info card). */\n\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] {\n display: flex !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n width: 100% !important;\n max-width: 100% !important;\n min-width: 0 !important;\n height: 28px !important;\n min-height: 28px !important;\n max-height: 28px !important;\n box-sizing: border-box !important;\n white-space: nowrap !important;\n overflow-x: auto !important;\n overflow-y: hidden !important;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior-x: contain;\n scrollbar-width: thin !important;\n scrollbar-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .28)) transparent !important;\n padding: 0 0 4px !important;\n line-height: 20px !important;\n font-size: 12px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar {\n height: 2px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar-thumb {\n background: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .3)) !important;\n border-radius: 2px !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"]::-webkit-scrollbar-track {\n background: transparent !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] > * {\n display: flex !important;\n flex: 0 0 auto !important;\n flex-flow: row nowrap !important;\n align-items: center !important;\n width: max-content !important;\n min-width: max-content !important;\n max-width: none !important;\n white-space: nowrap !important;\n margin-right: 12px !important;\n padding: 0 !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] > *:last-child {\n margin-right: 0 !important;\n }\n [data-slot=\"conversation.composer.dock\"] > [class$=\"_root\"] * {\n white-space: nowrap !important;\n }\n\n /* ---------- dsh-genui panel dock ----------\n The genui panel docks above the composer (conversation.input.dock,\n id genui-panel). On a phone its business-blue outline, generous chrome\n and single-line ellipsis read as an unfinished artifact: long titles\n truncate mid-word (\"\u2026default b\u00B7\u00B7\u00B7\") with the chevron glued to the\n ellipsis, and the pill crowds the composer. Mobile treatment: neutral\n card border matching the composer, tighter chrome so the full title\n fits, chevron with breathing room. Scoped to the mobile frame marker \u2014\n desktop keeps genui's own styling untouched. */\n\n [data-mobile-nav=\"frame\"] [data-genui-panel] {\n margin: 6px 12px 4px !important;\n border-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;\n border-radius: 12px !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelToggle\"] {\n padding: 7px 12px !important;\n gap: 8px !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelBadge\"] {\n padding: 0 7px !important;\n border-radius: 5px !important;\n font-size: 10.5px !important;\n line-height: 1.7 !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelTitle\"] {\n flex: 1 1 auto !important;\n min-width: 0 !important;\n font-size: 12.5px !important;\n line-height: 1.45 !important;\n }\n [data-mobile-nav=\"frame\"] [data-genui-panel] [class*=\"_panelChevron\"] {\n flex: none !important;\n margin-left: 0 !important;\n padding-left: 4px !important;\n }\n\n /* ---------- git-graph branch chip: inside the composer card ----------\n The branch chip (conversation.input.dock) floats between the dock rows\n and the input card; on a phone it reads as a stray capsule crowding the\n composer. A client effect (MobileNavOverlay) reparents the chip INTO\n the composer card; these rules pin it to the card's top-left and give\n the card a dedicated chip row. The card is position: relative by the\n official stylesheet, so the absolute anchor resolves against it. The\n plugin's own sheet sets all four offsets on the anchor, so right/bottom\n must be neutralized too. Scope is the frame marker + the anchor\n attribute (NOT the dock slot \u2014 the reparenting moves the chip out of\n the dock's subtree). Desktop untouched: the frame marker only exists\n below 1024px, and the effect restores the chip to the dock when the\n viewport widens. Chip row geometry (2026-08-16, user feedback): 48px\n padding left a 16px dead gap between the chip and the input line and\n made the composer read too tall; the row is now 40px = chip (24px) at\n top 12px + ~4px to the textarea \u2014 the chip sits slightly lower and\n the gap is compressed without touching the official height budget\n further. */\n\n [data-mobile-nav=\"frame\"] [data-gitgraph-chip-anchor] {\n position: absolute !important;\n top: 12px !important;\n left: 12px !important;\n right: auto !important;\n bottom: auto !important;\n z-index: 1 !important;\n }\n [data-mobile-nav=\"frame\"] [class$=\"_card\"]:has([data-gitgraph-chip-anchor]) {\n padding-top: 40px !important;\n }\n\n /* ---------- dsh-better-sidebar: safe area (S2.1, 2026-08-17) ----------\n THIRD-PARTY COMPAT RULE \u2014 dsh-better-sidebar (the workbench the session\n header's panel button opens). Its shell is viewport-fixed and starts at\n y=0: the panel at inset 0 (100vw drawer below 768px, a right column\n above it) and the toggle cluster at top:3px. Neither knows about\n env(safe-area-inset-*), so on a notched iPhone the whole tab strip \u2014\n including the one button that CLOSES the panel \u2014 sits behind the status\n bar and cannot be tapped: the user opened the workbench and was stuck\n there (real-device report, 2026-08-17).\n Applied across the plugin's whole mobile band, not just <768px: the\n panel is fixed at top:0 in the 768-1023px range too, so the same notch\n covers the same tab strip. Zero effect wherever the inset is 0 (every\n desktop browser, every non-notched device) \u2014 desktop is >=1024px and\n out of this media block entirely.\n Anchors: the plugin's own mount marker [data-dsh-better-sidebar]\n (index.tsx) plus class-suffix selectors, per this repo's hashed-class\n convention. --dsh-title-bar-strip is the plugin's own title-bar-compat\n offset (set only while that mode is on, 0px fallback otherwise): adding\n to it keeps both offsets rather than clobbering theirs.\n !important because their :global(body[...]) rules outrank a plain\n attribute selector.\n No box-sizing here on purpose: the panel is position:fixed with BOTH\n top and bottom set, so its used height already resolves to\n \"containing block - insets - padding - border\" (CSS 2.1 10.6.4) and the\n padding shrinks the content box without any help. Forcing border-box\n also folds their 1px left border into the inline width \u2014 measured as a\n 1px panel-width change at 768px, i.e. a regression outside this\n hotfix's remit. */\n [data-dsh-better-sidebar] [class$=\"_panel\"],\n [data-dsh-better-sidebar] [class$=\"_panelHidden\"] {\n padding-top: calc(var(--mnav-sat) + var(--dsh-title-bar-strip, 0px)) !important;\n }\n /* Only the tablet/desktop range still shows the cluster (see the hide\n rule below) \u2014 the notch offset is now dead weight below 768px, so it\n is scoped out rather than left applying invisibly. min-width:768px\n rather than the more common max-width pairing: this offset was\n already a harmless no-op at >=1024px before S3.1 (--mnav-sat is 0 on\n every desktop browser), so narrowing its floor to 768px changes\n nothing there either \u2014 it only stops evaluating on phone. */\n @media (min-width: 768px) {\n [data-dsh-better-sidebar] [class$=\"_toggleCluster\"] {\n top: calc(var(--mnav-sat) + var(--dsh-title-bar-strip, 0px) + 3px) !important;\n }\n }\n\n /* ---------- dsh-better-sidebar: hide the phone toggle cluster (S3.1, 2026-08-17) ----------\n Real-device round 2 feedback: this fixed top-right cluster duplicates\n \u2014 and visually overlaps \u2014 the session header's own workbench button\n (MobileSessionHeader.tsx), which already opens/closes the same panel\n by clicking this cluster's toggle button through. Hidden below 768px\n only; the 768-1023px tablet range has no workbench button (header.css.ts\n scopes that entire reflow to <768px) and still depends on this cluster\n as its only entry point, so it stays exactly as v1.0.0/S2.1 shipped it\n there. The cluster also holds the panel's own close affordance \u2014 see\n [data-mobile-nav=\"better-sidebar-close\"] below for the phone\n replacement, wired up in MobileSessionHeader.tsx. */\n @media (max-width: 767px) {\n [data-dsh-better-sidebar] [class$=\"_toggleCluster\"] {\n display: none !important;\n }\n }\n\n /* ---------- dsh-better-sidebar: phone close button (S3.1 follow-up, 2026-08-17) ----------\n Appended to document.body by MobileSessionHeader.tsx's\n MobileHeaderUtilities effect \u2014 never inside the panel's own subtree\n (the third party's React re-renders would wipe it) and never under any\n transformed/backdrop-filter ancestor (the S4 info-card WebKit lesson in\n AGENTS.md: position:fixed re-anchors to the nearest such ancestor\n instead of the viewport). Default hidden \u2014 belt-and-braces, same\n reasoning as header.css.ts's [data-mobile-nav=\"header-info\"] etc list:\n React does not know about media queries. Shown only below 768px AND\n only while the panel is actually open: the panel's own class name ends\n in \"_panel\" exclusively in the open state (the \"_panelHidden\" suffix is\n appended once closed, so the string no longer ends in \"_panel\") \u2014 a\n pure-CSS :has() open/closed read, no MutationObserver required.\n\n Bottom-center pill, not a top-right circle (real-device follow-up,\n 2026-08-17): a top-right position collided with the panel's own\n per-tab toolbar controls \u2014 measured live at 390px with the explorer\n tab open, the panel's Refresh button sits at x:354-382 y:93-121, and a\n 44px circle at top:8px+safe-area/right:8px lands at x:338-382\n y:(safe-area+8)-(safe-area+52), a direct overlap once the safe-area\n offset is small (or zero on non-notched phones). Every per-tab toolbar\n (explorer/git/tabBar) lives at the panel's TOP; nothing in the\n default explorer or git tabs reaches the bottom 90px of the viewport\n (checked live, both tabs, 2026-08-17), so bottom-center is clear\n regardless of which tab is open \u2014 one fixed position that does not\n need per-tab-type coordinates to dodge. */\n /* Default-hidden for this pill lives in header.css.ts, NOT here: this\n whole file sits inside the shared (max-width: 1023px) block, so a rule\n here can never hide anything at desktop widths (learned the hard way,\n 2026-08-17 desktop leak). */\n @media (max-width: 767px) {\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"] {\n display: flex !important;\n position: fixed;\n left: 50%;\n bottom: calc(var(--mnav-sab) + 12px);\n transform: translateX(-50%);\n z-index: 70;\n align-items: center;\n gap: 6px;\n height: 44px;\n padding: 0 18px;\n border: none;\n border-radius: 999px;\n background: var(--dsw-alias-bg-base, #fff);\n /* Drop shadow alone does not draw an edge: the fill is bg-base, which in\n dark theme is the same colour as the surface behind it, and a black\n shadow on a dark ground is invisible \u2014 the pill read as floating text\n with no boundary. The inset hairline is the edge; the drop shadow only\n lifts it. Same token the session rows use for their dark-mode ring. */\n box-shadow:\n inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, .14)),\n 0 4px 16px rgba(0, 0, 0, .2);\n color: var(--dsw-alias-label-primary, inherit);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"] svg {\n width: 14px;\n height: 14px;\n flex: none;\n }\n body:has([data-dsh-better-sidebar] [class$=\"_panel\"]) [data-mobile-nav=\"better-sidebar-close\"]:active {\n background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));\n }\n }\n\n /* ---------- dsh-agent-teams: phone floater reposition (2026-08-18) ----------\n THIRD-PARTY COMPAT RULE \u2014 @nanmicoder/dsh-agent-teams (the AgentTeams\n activity floater: a collapsed badge + expandable panel, body-portaled\n under its own mount marker div[data-agent-teams-host]). Its own CSS\n fixes both at top:56px (<=640px) viewport coordinates and never reads\n env(safe-area-inset-*), so on a notched iPhone the badge lands INSIDE\n the safe-area + this plugin's 76px session header band \u2014 overlapping\n the info and workbench buttons (real-device report, 2026-08-18).\n Move both below the header instead: safe-area + 76px header chrome\n (48px title row + 28px view-switch row, header.css.ts) + 8px gap.\n Anchored on the plugin's own mount marker + class-suffix selectors per\n this repo's hashed-class convention \u2014 when dsh-agent-teams is not\n installed the marker never exists and this whole section is inert\n (same presence-gating as the dsh-better-sidebar rules above).\n !important because their positions come from html-level CSS vars\n (--agent-teams-panel-top et al.) whose style tag order vs ours is not\n guaranteed. z-index: their 2147483000 would float the badge over every\n one of this plugin's own overlays (drawer 40, info sheet 55-57, close\n pill 70); 30 keeps it above content but below all of them. */\n @media (max-width: 767px) {\n [data-agent-teams-host] [class$=\"_badge\"],\n [data-agent-teams-host] [class$=\"_panel\"] {\n top: calc(var(--mnav-sat) + 84px) !important;\n right: 10px !important;\n z-index: 30 !important;\n }\n [data-agent-teams-host] [class$=\"_panel\"] {\n left: 10px !important;\n width: auto !important;\n /* Their min-height:min(560px, calc(100dvh - 56 - 56)) resolves against\n the OLD top var \u2014 on short phones it exceeds the space left below\n the moved-down top edge and would force overflow; content-sized with\n a hard cap replaces it. 76px bottom clearance keeps the collapsed\n composer's top edge visible under the panel. */\n min-height: 0 !important;\n max-height: calc(100dvh - var(--mnav-sat) - 84px - var(--mnav-sab) - 76px) !important;\n }\n /* The floater belongs to the SESSION page: on the home list it hovered\n over the session rows (user feedback, 2026-08-18). The phone page\n stack is this plugin's own overlay \u2014 the official conversation (and\n with it the floater's \"current session\") stays mounted underneath, so\n agent-teams keeps rendering it; hide it whenever the home level is the\n visible one (data-view flips to \"session\" inside a session, and the\n badge/panel come right back). :has() is already a documented\n requirement of this plugin (Chromium 105+, docs/interface.md). */\n body:has([data-mobile-nav=\"home\"][data-view=\"home\"]) [data-agent-teams-host] [class$=\"_badge\"],\n body:has([data-mobile-nav=\"home\"][data-view=\"home\"]) [data-agent-teams-host] [class$=\"_panel\"] {\n display: none !important;\n }\n }\n\n /* ---------- dsh-at-file compatibility ----------\n dsh-at-file registers its own `conversation.input.dock` entry\n ([data-at-file-dock]) and renders one row per `@path` token in the\n draft. That is exactly the same source this plugin's attachment chips\n read, so on a phone a single uploaded image produced TWO entries on the\n composer rail: our 48px thumbnail tile AND at-file's file-name row for\n the same token (reported 2026-08-22).\n\n Drop at-file's row for the upload directory only. Everything else the\n user @-mentions by hand is at-file's own job and stays \u2014 this is not a\n blanket hide. The path is readable from the row's own title attribute\n (FilesDock passes `title={mention.relative}`), which is a rendered\n contract rather than a hashed class name.\n\n Phone only, and deliberately so: at >= 768px our chips are hidden by\n composer.css.ts, which leaves at-file's row as the ONLY thing on screen\n representing the attachment. Hiding it there would make an uploaded file\n invisible.\n\n When every row was an upload the container itself is left empty, and an\n empty flex item still claims the rail's 6px gap \u2014 so the dock goes too\n unless at least one non-upload row survives. */\n @media (max-width: 767px) {\n [data-at-file-dock] [data-at-file-row]:has([title^=\".dsh-uploads/\"]) {\n display: none !important;\n }\n [data-at-file-dock]:not(:has([data-at-file-row] [title]:not([title^=\".dsh-uploads/\"]))) {\n display: none !important;\n }\n }\n /* ---------- @ace-zone/dsh-market: modal header fits a phone (2026-08-22) ----------\n THIRD-PARTY COMPAT RULE \u2014 @ace-zone/dsh-market (the plugin market:\n a settings-page entry that opens its own body-level modal,\n .dshm-overlay > .dshm-panel, over the settings sheet).\n\n Its header (.dshm-head) is a plain flex row with no wrapping and no\n shrinking: title, a badge strip whose slogan is white-space:nowrap,\n a version chip, an \"official site\" link, a language toggle, and only\n then the \u00D7 that closes the modal. Flex items default to\n min-width:auto, so at phone widths the row keeps its intrinsic size,\n .dshm-panel clips it (overflow:hidden), and the \u00D7 \u2014 the only way out\n of the modal on a touch screen, which has no Esc key \u2014 is off screen\n (reported 2026-08-22).\n\n Drop the three decorative items and let the title ellipsis instead.\n The language toggle stays: it is a real control, and title + toggle +\n \u00D7 fit inside 320px once the title can shrink. The title rule also\n covers the job / help overlays, which reuse .dshm-head with a long\n \"<action> \u00B7 <package>\" title and no flexible item of their own.\n Nothing here is desktop-side: at >= 768px the row has the width it\n was written for. */\n @media (max-width: 767px) {\n body:has(.dshm-ver[title=\"@ace-zone/dsh-market\"]) .dshm-head > .dshm-head-badges,\n body:has(.dshm-ver[title=\"@ace-zone/dsh-market\"]) .dshm-head > .dshm-ver,\n body:has(.dshm-ver[title=\"@ace-zone/dsh-market\"]) .dshm-head > a.dshm-viewbtn {\n display: none !important;\n }\n body:has(.dshm-ver[title=\"@ace-zone/dsh-market\"]) .dshm-head > .dshm-title {\n flex: 1 1 auto !important;\n min-width: 0 !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n white-space: nowrap !important;\n }\n /* 44px touch target: their 20px glyph in 2px/6px padding is ~30px. */\n body:has(.dshm-ver[title=\"@ace-zone/dsh-market\"]) .dshm-head > .dshm-close {\n flex: 0 0 auto !important;\n font-size: 24px !important;\n padding: 6px 12px !important;\n }\n }\n";
2
2
  //# sourceMappingURL=compat.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compat.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/compat.css.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,qnqCA8vBtB,CAAA"}
1
+ {"version":3,"file":"compat.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/compat.css.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,UAAU,4xuCAuyBtB,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 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 (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,k2tBAuftB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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
@@ -3,6 +3,17 @@
3
3
 
4
4
  import { UPLOAD_DIR } from '../attach-upload.ts'
5
5
 
6
+ /* Presence gate for @ace-zone/dsh-market. Its markup carries no mount
7
+ marker and its `dshm-` class prefix is not proof of identity — several
8
+ unrelated plugins call themselves a "dsh market". The version chip in
9
+ the modal header is rendered as
10
+ `<span class="dshm-ver" title="@ace-zone/dsh-market">`, i.e. the exact
11
+ package name as a rendered contract, so it is the one selector that
12
+ cannot match anything else. Every rule below hangs off it: with the
13
+ plugin absent (or its modal closed) nothing matches and the section is
14
+ inert. */
15
+ const MARKET = '.dshm-ver[title="@ace-zone/dsh-market"]'
16
+
6
17
  export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
7
18
  The linxin666 plugin suite extends the shell frame directly:
8
19
  - aionui-panel appends two trailing grid columns (explorer / preview)
@@ -769,4 +780,45 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
769
780
  display: none !important;
770
781
  }
771
782
  }
783
+ /* ---------- @ace-zone/dsh-market: modal header fits a phone (2026-08-22) ----------
784
+ THIRD-PARTY COMPAT RULE — @ace-zone/dsh-market (the plugin market:
785
+ a settings-page entry that opens its own body-level modal,
786
+ .dshm-overlay > .dshm-panel, over the settings sheet).
787
+
788
+ Its header (.dshm-head) is a plain flex row with no wrapping and no
789
+ shrinking: title, a badge strip whose slogan is white-space:nowrap,
790
+ a version chip, an "official site" link, a language toggle, and only
791
+ then the × that closes the modal. Flex items default to
792
+ min-width:auto, so at phone widths the row keeps its intrinsic size,
793
+ .dshm-panel clips it (overflow:hidden), and the × — the only way out
794
+ of the modal on a touch screen, which has no Esc key — is off screen
795
+ (reported 2026-08-22).
796
+
797
+ Drop the three decorative items and let the title ellipsis instead.
798
+ The language toggle stays: it is a real control, and title + toggle +
799
+ × fit inside 320px once the title can shrink. The title rule also
800
+ covers the job / help overlays, which reuse .dshm-head with a long
801
+ "<action> · <package>" title and no flexible item of their own.
802
+ Nothing here is desktop-side: at >= 768px the row has the width it
803
+ was written for. */
804
+ @media (max-width: 767px) {
805
+ body:has(${MARKET}) .dshm-head > .dshm-head-badges,
806
+ body:has(${MARKET}) .dshm-head > .dshm-ver,
807
+ body:has(${MARKET}) .dshm-head > a.dshm-viewbtn {
808
+ display: none !important;
809
+ }
810
+ body:has(${MARKET}) .dshm-head > .dshm-title {
811
+ flex: 1 1 auto !important;
812
+ min-width: 0 !important;
813
+ overflow: hidden !important;
814
+ text-overflow: ellipsis !important;
815
+ white-space: nowrap !important;
816
+ }
817
+ /* 44px touch target: their 20px glyph in 2px/6px padding is ~30px. */
818
+ body:has(${MARKET}) .dshm-head > .dshm-close {
819
+ flex: 0 0 auto !important;
820
+ font-size: 24px !important;
821
+ padding: 6px 12px !important;
822
+ }
823
+ }
772
824
  `
@@ -267,48 +267,40 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
267
267
  [data-phase] header [data-slot="conversation.session.header.utilities"] > * {
268
268
  display: none !important;
269
269
  }
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.
270
+ /* Native background-task / subagent entries (rewritten for DSH 0.1.1,
271
+ 2026-08-21). Three upstream changes killed the old "park the root as a
272
+ zero-width anchor, hide the trigger, forward the tap" design at once:
275
273
 
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.
274
+ - the subagent entry moved out of header.actions into the new
275
+ conversation.session.header.lineage slot (inside the breadcrumb);
276
+ - its root's class attribute is now "ZKlsPq_root " with a TRAILING
277
+ SPACE so a [class$="_root"] anchor matches nothing at all;
278
+ - the trigger only reacts to TRUSTED input: .click() and synthetic
279
+ pointer/mouse events leave aria-expanded untouched (measured on device
280
+ with every plugin override stripped), so forwarding a tap in JS can
281
+ never open it again.
280
282
 
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"] {
283
+ The trigger's geometry is therefore owned at runtime by
284
+ effects/native-trigger-overlay.ts, which lays the REAL button
285
+ transparently over the activity pill a genuine finger tap lands on the
286
+ official element. Nothing in this stylesheet may size, hide, or set
287
+ pointer-events on the trigger: that is exactly what would take the tap
288
+ target away again.
289
+
290
+ CSS keeps only two chores. Dissolve the root so its box (and its
291
+ stray separator, hidden below) stops occupying the breadcrumb, and let
292
+ the trigger participate from wherever the overlay pins it. And no menu
293
+ re-anchoring anymore: the 0.1.1 popover portals to <body> as
294
+ position:fixed and positions itself from the trigger's viewport rect
295
+ (measured 336px wide, fully on screen at 411px). */
296
+ [data-phase] header [data-slot="conversation.session.header.actions"] > *:has(> button[class$="_trigger"]),
297
+ [data-phase] header [data-slot="conversation.session.header.lineage"] > *:has(> button[class$="_trigger"]) {
298
+ display: contents !important;
299
+ }
300
+ [data-phase] header [data-slot="conversation.session.header.actions"] [class$="_separator"],
301
+ [data-phase] header [data-slot="conversation.session.header.lineage"] [class$="_separator"] {
297
302
  display: none !important;
298
303
  }
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
304
  /* The agent-preset mode badge specifically survives the blanket hide
313
305
  above: layout.css.ts's shared <=1023px block targets it directly
314
306
  (header [class$="_label"]:has(> svg) { display: block !important }),
@@ -433,7 +425,11 @@ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768
433
425
  touch-action: manipulation;
434
426
  -webkit-tap-highlight-color: transparent;
435
427
  }
428
+ /* pointer-events:none so the invisible official trigger laid over this pill
429
+ (effects/native-trigger-overlay.ts) is what a tap reaches. The pill is
430
+ visuals only. */
436
431
  [data-mobile-nav="activity-pill"] {
432
+ pointer-events: none;
437
433
  display: inline-flex;
438
434
  align-items: center;
439
435
  gap: 3px;