dsh-zen-remote 1.1.9 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.
|
|
6
|
+
<img src="https://img.shields.io/badge/release-v1.1.10-5B4CF0?style=flat-square" alt="v1.1.10">
|
|
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.
|
|
42
|
+
"dsh-zen-remote": "^1.1.10" // 本地开发换成 "link:/path/to/dsh-zen-remote"
|
|
43
43
|
},
|
|
44
44
|
"dsh": { "profile": { "bundles": [
|
|
45
45
|
"@deepseek-ai/dsh-base",
|
|
@@ -188,6 +188,7 @@ open http://127.0.0.1:3088/lan-gate/admin
|
|
|
188
188
|
| `DSH_PUSH_DEBOUNCE_MS` | `15000` | 两条自动推送的最小间隔;等授权/等回答的通知不受压制 |
|
|
189
189
|
| `DSH_PUSH_SUMMARY` | 关 | 设 `1` 让通知带上本回合的最终回复(只取正文,不含思考过程;截 120 字)和提问原文 |
|
|
190
190
|
| `DSH_PUSH_TOOL` | 开 | 设 `0` 关掉模型可调用的 `push_notify` 工具 |
|
|
191
|
+
| `DSH_PUSH_APPROVAL_GRACE_MS` | `5000` | 「等授权」推送前的等待窗口。装了会自动答复审批的插件时(如 dsh-auto-approve),要等它答完再决定推不推——答完了就不推。判定器比这个慢就还是会推,那时把它调大 |
|
|
191
192
|
|
|
192
193
|
上传大小上限(默认 20MB)在插件行的 `config.maxUploadBytes` 里改。
|
|
193
194
|
|
|
@@ -234,6 +235,13 @@ open http://127.0.0.1:3088/lan-gate/admin
|
|
|
234
235
|
这两类不看会话层级——子代理自己卡在授权上,照样喊你,因为等的还是你。也**不受
|
|
235
236
|
`DSH_PUSH_DEBOUNCE_MS` 压制**:「有操作等你点头」是最不能被吞掉的一条。
|
|
236
237
|
|
|
238
|
+
**有机器答复者时的时机**:审批事件的顺序是「先记 asked → 问答复者 → 记 decided」,
|
|
239
|
+
所以推送并不是一见到 asked 就发,而是等 `DSH_PUSH_APPROVAL_GRACE_MS`(默认 5 秒)
|
|
240
|
+
——这段时间内被答复掉的就不推。这个窗口原来是 1.5 秒,按「答复者都在同一个 tick
|
|
241
|
+
内结算」设计的;那对同步答复者成立,但对模型答复者不成立(实测平均 2.4 秒),
|
|
242
|
+
结果是自动通过的请求照样推了一条「等你授权」,通知到了、框却从来没出现。
|
|
243
|
+
换了更慢的判定模型就把这个值调大。
|
|
244
|
+
|
|
237
245
|
策略自动放行的授权不会打扰你:请求发起后先等 1.5 秒,配对的「已决定」到了就取消,
|
|
238
246
|
只有真正悬着没人管的才推。
|
|
239
247
|
|
package/docs/interface.md
CHANGED
|
@@ -119,6 +119,9 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
|
|
|
119
119
|
|
|
120
120
|
**修复**
|
|
121
121
|
|
|
122
|
+
- 适配 dsh-vision-router(2.0.1):composer 识图开关在手机端压成 28px 圆形
|
|
123
|
+
图标钮(眼睛为 CSS mask 图标,aria-label / aria-pressed 照旧),不再把
|
|
124
|
+
模型名挤出 composer 行;桌面端保持插件原样(见兼容插件清单)。
|
|
122
125
|
- 适配 DSH `0.1.1`:活动 chip 点击失效(点了退回信息卡)。三处上游变化同时命中
|
|
123
126
|
旧方案——子代理入口从 `header.actions` 槽移进新的 `header.lineage` 槽(面包屑
|
|
124
127
|
内);其根节点 class 变成 `"ZKlsPq_root "`(**带尾随空格**,`[class$="_root"]`
|
|
@@ -245,6 +248,7 @@ composer 最左的回形针打开的是**手机本地**的文件选择器(iOS
|
|
|
245
248
|
- [@opendsh/dsh-plugin-scheduled-tasks](https://github.com/Ceelog/dsh-plugins)(定时任务,主屏 chips 行自动收割入口)——本机实测 **0.2.3**
|
|
246
249
|
- dsh-at-file(@文件引用;它和本插件的附件 chip 读的是同一份草稿 token,手机端会为一个上传文件同时画出缩略图和文件名两条,故隐藏它 `.dsh-uploads/` 下那几行,其余 @ 引用不动;桌面端不隐藏——那边本插件的 chip 是关掉的,它是唯一的呈现)——本机实测 **0.6.7**
|
|
247
250
|
- [@ace-zone/dsh-market](https://www.npmjs.com/package/@ace-zone/dsh-market)(插件市场;它的弹窗顶栏在手机上放不下,关闭的 × 被挤出面板外,故隐藏标语/版本号/官网链接三个装饰位并让标题省略号收缩,语言切换和 × 保留;认插件自己渲染的 `.dshm-ver[title="@ace-zone/dsh-market"]` 版本片,同名的其他"市场"插件不会被误伤)——本机实测 **0.1.66**
|
|
251
|
+
- [dsh-vision-router](https://github.com/ysr666/dsh-vision-router)(视觉桥,composer 行内的识图开关胶囊;手机端压成 28px 圆形图标钮——原胶囊 👁+文字+激活 ✓ 约 90px 定宽,而手机 composer 行不换行、模型座位是唯一可收缩项,会被它挤没;三条文字 span 全部隐藏,改由 CSS mask 画一只 currentColor 眼睛,开合状态仍走插件自己的品牌描边/着色与 `aria-pressed`,可访问名走按钮自带的 aria-label)——本机实测 **2.0.1**
|
|
248
252
|
- [dsh-web-ui 全家桶](https://www.npmjs.com/package/@linxin666/dsh-web-ui-all)(文件树 / 预览 / 任务看板 / SSH / 宠物 / 会话统计 / 远程配对 / 设置)——沿用上游兼容规则,本次未扩展
|
|
249
253
|
|
|
250
254
|
## 安装
|
package/dsh-push.mjs
CHANGED
|
@@ -61,10 +61,24 @@ const PUSH_TOOL_ENABLED = process.env.DSH_PUSH_TOOL !== undefined ? process.env.
|
|
|
61
61
|
const ASK_USER_TOOL = 'ask_user_question'
|
|
62
62
|
|
|
63
63
|
// @deepseek-ai/dsh-user-approval appends `approval/asked` before consulting
|
|
64
|
-
// the answerer chain and `approval/decided` after
|
|
65
|
-
// hook-answered request settles in the same tick, so only an ask still
|
|
64
|
+
// the answerer chain and `approval/decided` after — so only an ask still
|
|
66
65
|
// undecided after this grace period is actually waiting on a human.
|
|
67
|
-
|
|
66
|
+
//
|
|
67
|
+
// The window used to be 1500ms on the assumption that "a policy-resolved or
|
|
68
|
+
// hook-answered request settles in the same tick". That held while every
|
|
69
|
+
// answerer was synchronous. A model-backed answerer (dsh-auto-approve and
|
|
70
|
+
// anything like it) takes seconds: measured 2.4s average, 3.4s worst on a
|
|
71
|
+
// small model, and more on a slower one. At 1500ms the timer won the race and
|
|
72
|
+
// pushed "waiting for your approval" for requests that were auto-approved a
|
|
73
|
+
// second later — the notification arrived, the prompt never did.
|
|
74
|
+
//
|
|
75
|
+
// 5000ms covers a machine answerer with margin while still reaching a phone
|
|
76
|
+
// promptly when a human really is needed. Raise it if your answerer is slower
|
|
77
|
+
// than that; the cost of a longer window is only that genuine prompts notify
|
|
78
|
+
// later.
|
|
79
|
+
const APPROVAL_PENDING_MS = Number(
|
|
80
|
+
process.env.DSH_PUSH_APPROVAL_GRACE_MS ?? FILE.pushApprovalGraceMs ?? 5000
|
|
81
|
+
)
|
|
68
82
|
|
|
69
83
|
// Low-level sender shared by every leg: POSTs to the gateway's local
|
|
70
84
|
// /pwa/push/send, which does the actual VAPID + aes128gcm encrypted delivery
|
|
@@ -415,7 +429,7 @@ export function apply(ctx) {
|
|
|
415
429
|
console.warn(`[dsh-zen-remote-push] cannot listen on "session/event": ${String(e && e.message || e)}`)
|
|
416
430
|
}
|
|
417
431
|
|
|
418
|
-
console.log(`[dsh-zen-remote-push] approval/question notifications on; turn-end (${EVENTS.join(', ')}) ${TURN_END_ENABLED ? 'on' : 'off — set DSH_PUSH_TURN_END=1 to enable'}`)
|
|
432
|
+
console.log(`[dsh-zen-remote-push] approval/question notifications on (approval grace ${APPROVAL_PENDING_MS}ms); turn-end (${EVENTS.join(', ')}) ${TURN_END_ENABLED ? 'on' : 'off — set DSH_PUSH_TURN_END=1 to enable'}`)
|
|
419
433
|
|
|
420
434
|
registerPushTool(ctx, gate)
|
|
421
435
|
}
|
package/lib/client.js
CHANGED
|
@@ -3467,6 +3467,91 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
|
|
|
3467
3467
|
mask-repeat: no-repeat;
|
|
3468
3468
|
}
|
|
3469
3469
|
}
|
|
3470
|
+
|
|
3471
|
+
/* ---------- dsh-vision-router: composer vision toggle (2026-08-26) ----------
|
|
3472
|
+
THIRD-PARTY COMPAT RULE — dsh-vision-router registers a Vision mode
|
|
3473
|
+
toggle into conversation.input.right (lib/client-presentation-boundary-
|
|
3474
|
+
main.js, slot id "vision-router-mode-toggle"): a bordered pill with
|
|
3475
|
+
THREE text spans (an eye emoji, the localized label, and a check mark
|
|
3476
|
+
while active) sized by INLINE styles — minHeight 28, padding 4px 8px,
|
|
3477
|
+
fontSize 12, white-space nowrap — roughly 90px of unshrinkable width.
|
|
3478
|
+
|
|
3479
|
+
The phone composer row is nowrap by design (composer.css.ts section 1)
|
|
3480
|
+
and the model seat is its only shrinkable item, so that pill squeezes
|
|
3481
|
+
the model name out of the row (user report, 2026-08-26).
|
|
3482
|
+
|
|
3483
|
+
Phone treatment: a 28x28 round icon button in the attach button's
|
|
3484
|
+
language (same size, same neutral fill). All three spans are hidden and
|
|
3485
|
+
ONE 16px eye is drawn by a CSS mask on ::before with currentColor — the
|
|
3486
|
+
same survives-React-re-render reasoning as the auto-approve shield
|
|
3487
|
+
below: the button re-renders on every state flip, so a DOM-injected
|
|
3488
|
+
icon would need an observer while generated content does not. The
|
|
3489
|
+
accessible name is unaffected: the button ships aria-label + title with
|
|
3490
|
+
the full enable/disable copy, and aria-pressed keeps conveying the
|
|
3491
|
+
state; visually the plugin's own inline styles still flip border +
|
|
3492
|
+
background + color to the brand token while active (only the inactive
|
|
3493
|
+
look is restyled to match the row).
|
|
3494
|
+
|
|
3495
|
+
Presence gate: [data-vision-router-mode-toggle] is rendered by the
|
|
3496
|
+
plugin itself (the exact attribute from its createElement call) — with
|
|
3497
|
+
the plugin absent the attribute never exists and this whole section is
|
|
3498
|
+
inert, not one selector matching (same convention as the
|
|
3499
|
+
dsh-better-sidebar rules). !important throughout because the geometry
|
|
3500
|
+
it replaces is inline style. Phone only (max-width: 767px): desktop
|
|
3501
|
+
keeps the plugin's full pill untouched, and the 768-1023px tablet row
|
|
3502
|
+
has the width for it. order:5 restates composer.css.ts's generic
|
|
3503
|
+
input.right rule with the same value — kept here so the toggle can
|
|
3504
|
+
never fall back to order 0 (far left, before the model) if that
|
|
3505
|
+
structural selector ever stops matching. */
|
|
3506
|
+
@media (max-width: 767px) {
|
|
3507
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] {
|
|
3508
|
+
order: 5 !important;
|
|
3509
|
+
flex: 0 0 auto !important;
|
|
3510
|
+
box-sizing: border-box !important;
|
|
3511
|
+
width: 28px !important;
|
|
3512
|
+
min-width: 28px !important;
|
|
3513
|
+
max-width: 28px !important;
|
|
3514
|
+
height: 28px !important;
|
|
3515
|
+
min-height: 28px !important;
|
|
3516
|
+
padding: 0 !important;
|
|
3517
|
+
gap: 0 !important;
|
|
3518
|
+
justify-content: center !important;
|
|
3519
|
+
border-radius: 999px !important;
|
|
3520
|
+
touch-action: manipulation !important;
|
|
3521
|
+
-webkit-tap-highlight-color: transparent;
|
|
3522
|
+
}
|
|
3523
|
+
/* Inactive look: neutral round fill like the attach button — the
|
|
3524
|
+
plugin's own inactive state would draw a lonely outline circle that
|
|
3525
|
+
reads as broken next to the row's filled icon buttons. */
|
|
3526
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:not([aria-pressed="true"]) {
|
|
3527
|
+
border-color: transparent !important;
|
|
3528
|
+
background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
|
|
3529
|
+
box-shadow: none !important;
|
|
3530
|
+
}
|
|
3531
|
+
/* Eye emoji + label + active check: all three spans out. */
|
|
3532
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > span {
|
|
3533
|
+
display: none !important;
|
|
3534
|
+
}
|
|
3535
|
+
/* The one svg: eye outline + iris as a currentColor mask — brand while
|
|
3536
|
+
active, dimmed with the button by the plugin's own disabled opacity. */
|
|
3537
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]::before {
|
|
3538
|
+
content: '';
|
|
3539
|
+
width: 16px;
|
|
3540
|
+
height: 16px;
|
|
3541
|
+
flex: none;
|
|
3542
|
+
background: currentColor;
|
|
3543
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E");
|
|
3544
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E");
|
|
3545
|
+
-webkit-mask-size: contain;
|
|
3546
|
+
mask-size: contain;
|
|
3547
|
+
-webkit-mask-repeat: no-repeat;
|
|
3548
|
+
mask-repeat: no-repeat;
|
|
3549
|
+
}
|
|
3550
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:focus-visible {
|
|
3551
|
+
outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
|
|
3552
|
+
outline-offset: 2px;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3470
3555
|
`;
|
|
3471
3556
|
};
|
|
3472
3557
|
__modules["styles/misc.css.js"] = function (require, module, exports) {
|
|
@@ -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 /* ---------- @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\n /* ---------- dsh-auto-approve compatibility ----------\n The composer.css.ts phone rules collapse the permission trigger to an\n icon-only pill, but the official permissionGlyphs table only draws\n icons for read-only / workspace-write \u2014 the auto-approve plugin's\n \"auto\" preset gets none, so its pill showed a bare chevron. The plugin\n injects its shield+sparkle svg into the MENU items itself (DOM,\n content-anchored), but the collapsed TRIGGER re-renders on every\n preset change, so a CSS pseudo-element is the survives-re-render fix\n (same reasoning as the old model Sparkle ::before above).\n\n Presence gates, honestly stated (2026-08-25 review):\n - Trigger rule: aria-label is t('input.accessMode', {name}) with the\n raw preset display name embedded untranslated and (zh/en verified)\n at the END of the string \u2014 [aria-label$=\"Auto\"] matches exactly the\n auto-approve preset being selected, and cannot match a future\n \"Autopilot\"-style preset. Plugin absent \u2192 the name never renders \u2192\n inert. This is a DIRECT gate on the plugin's own preset name.\n - Menu-item rule: INDIRECT gate only. It matches \"permission-menu item\n without an official icon span\"; the official three presets all ship\n icon spans in the menu (verified live), so with the plugin absent\n nothing matches. Known boundary: another plugin's icon-less preset,\n or a host redesign dropping icon spans, would wrongly receive this\n shield. A direct gate is not possible: menu items carry no\n id/data attributes to anchor on, CSS cannot match text, and gating\n on the plugin's own JS-injected markers would kill this fallback in\n exactly the JS-did-not-run case it exists for. Paths are the plugin's own\n shield outline + four-point sparkle (permission-glyph.ts), stroke\n shield + filled sparkle, drawn as a currentColor mask. */\n @media (max-width: 767px) {\n /* Fallback for the MENU items too (real-phone report 2026-08-25: the\n plugin's DOM injection did not run in that page instance while this\n CSS did \u2014 bundle load timing differs per plugin). Any permission-menu\n item WITHOUT the official icon span gets the shield drawn by CSS;\n today that is exactly the plugin \"auto\" preset (the official three\n all ship icons in the menu). Self-coordinating with the plugin's own\n injection: once its svg lands it arrives inside a cloned icon span,\n the :not() stops matching, no double icon. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] button[role=\"menuitem\"]:not(:has([class*=\"_itemIcon_\"]))::before,\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"][aria-label$=\"Auto\"]::before {\n content: '';\n width: 16px;\n height: 16px;\n flex: none;\n background: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n }\n }\n";
|
|
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\n /* ---------- dsh-auto-approve compatibility ----------\n The composer.css.ts phone rules collapse the permission trigger to an\n icon-only pill, but the official permissionGlyphs table only draws\n icons for read-only / workspace-write \u2014 the auto-approve plugin's\n \"auto\" preset gets none, so its pill showed a bare chevron. The plugin\n injects its shield+sparkle svg into the MENU items itself (DOM,\n content-anchored), but the collapsed TRIGGER re-renders on every\n preset change, so a CSS pseudo-element is the survives-re-render fix\n (same reasoning as the old model Sparkle ::before above).\n\n Presence gates, honestly stated (2026-08-25 review):\n - Trigger rule: aria-label is t('input.accessMode', {name}) with the\n raw preset display name embedded untranslated and (zh/en verified)\n at the END of the string \u2014 [aria-label$=\"Auto\"] matches exactly the\n auto-approve preset being selected, and cannot match a future\n \"Autopilot\"-style preset. Plugin absent \u2192 the name never renders \u2192\n inert. This is a DIRECT gate on the plugin's own preset name.\n - Menu-item rule: INDIRECT gate only. It matches \"permission-menu item\n without an official icon span\"; the official three presets all ship\n icon spans in the menu (verified live), so with the plugin absent\n nothing matches. Known boundary: another plugin's icon-less preset,\n or a host redesign dropping icon spans, would wrongly receive this\n shield. A direct gate is not possible: menu items carry no\n id/data attributes to anchor on, CSS cannot match text, and gating\n on the plugin's own JS-injected markers would kill this fallback in\n exactly the JS-did-not-run case it exists for. Paths are the plugin's own\n shield outline + four-point sparkle (permission-glyph.ts), stroke\n shield + filled sparkle, drawn as a currentColor mask. */\n @media (max-width: 767px) {\n /* Fallback for the MENU items too (real-phone report 2026-08-25: the\n plugin's DOM injection did not run in that page instance while this\n CSS did \u2014 bundle load timing differs per plugin). Any permission-menu\n item WITHOUT the official icon span gets the shield drawn by CSS;\n today that is exactly the plugin \"auto\" preset (the official three\n all ship icons in the menu). Self-coordinating with the plugin's own\n injection: once its svg lands it arrives inside a cloned icon span,\n the :not() stops matching, no double icon. */\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] [role=\"menu\"] button[role=\"menuitem\"]:not(:has([class*=\"_itemIcon_\"]))::before,\n [data-slot=\"conversation.composer.bar\"] [class$=\"_card\"] > [class$=\"_row\"] > [class$=\"_tools\"] > [class$=\"_modes\"] button[class$=\"_trigger\"][aria-label$=\"Auto\"]::before {\n content: '';\n width: 16px;\n height: 16px;\n flex: none;\n background: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' fill='none' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8.35 5.4C8.35 7.2 7.15 8.4 5.35 8.4C7.15 8.4 8.35 9.6 8.35 11.4C8.35 9.6 9.55 8.4 11.35 8.4C9.55 8.4 8.35 7.2 8.35 5.4Z'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n }\n }\n\n /* ---------- dsh-vision-router: composer vision toggle (2026-08-26) ----------\n THIRD-PARTY COMPAT RULE \u2014 dsh-vision-router registers a Vision mode\n toggle into conversation.input.right (lib/client-presentation-boundary-\n main.js, slot id \"vision-router-mode-toggle\"): a bordered pill with\n THREE text spans (an eye emoji, the localized label, and a check mark\n while active) sized by INLINE styles \u2014 minHeight 28, padding 4px 8px,\n fontSize 12, white-space nowrap \u2014 roughly 90px of unshrinkable width.\n\n The phone composer row is nowrap by design (composer.css.ts section 1)\n and the model seat is its only shrinkable item, so that pill squeezes\n the model name out of the row (user report, 2026-08-26).\n\n Phone treatment: a 28x28 round icon button in the attach button's\n language (same size, same neutral fill). All three spans are hidden and\n ONE 16px eye is drawn by a CSS mask on ::before with currentColor \u2014 the\n same survives-React-re-render reasoning as the auto-approve shield\n below: the button re-renders on every state flip, so a DOM-injected\n icon would need an observer while generated content does not. The\n accessible name is unaffected: the button ships aria-label + title with\n the full enable/disable copy, and aria-pressed keeps conveying the\n state; visually the plugin's own inline styles still flip border +\n background + color to the brand token while active (only the inactive\n look is restyled to match the row).\n\n Presence gate: [data-vision-router-mode-toggle] is rendered by the\n plugin itself (the exact attribute from its createElement call) \u2014 with\n the plugin absent the attribute never exists and this whole section is\n inert, not one selector matching (same convention as the\n dsh-better-sidebar rules). !important throughout because the geometry\n it replaces is inline style. Phone only (max-width: 767px): desktop\n keeps the plugin's full pill untouched, and the 768-1023px tablet row\n has the width for it. order:5 restates composer.css.ts's generic\n input.right rule with the same value \u2014 kept here so the toggle can\n never fall back to order 0 (far left, before the model) if that\n structural selector ever stops matching. */\n @media (max-width: 767px) {\n [data-slot=\"conversation.composer.bar\"] [data-vision-router-mode-toggle] {\n order: 5 !important;\n flex: 0 0 auto !important;\n box-sizing: border-box !important;\n width: 28px !important;\n min-width: 28px !important;\n max-width: 28px !important;\n height: 28px !important;\n min-height: 28px !important;\n padding: 0 !important;\n gap: 0 !important;\n justify-content: center !important;\n border-radius: 999px !important;\n touch-action: manipulation !important;\n -webkit-tap-highlight-color: transparent;\n }\n /* Inactive look: neutral round fill like the attach button \u2014 the\n plugin's own inactive state would draw a lonely outline circle that\n reads as broken next to the row's filled icon buttons. */\n [data-slot=\"conversation.composer.bar\"] [data-vision-router-mode-toggle]:not([aria-pressed=\"true\"]) {\n border-color: transparent !important;\n background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;\n box-shadow: none !important;\n }\n /* Eye emoji + label + active check: all three spans out. */\n [data-slot=\"conversation.composer.bar\"] [data-vision-router-mode-toggle] > span {\n display: none !important;\n }\n /* The one svg: eye outline + iris as a currentColor mask \u2014 brand while\n active, dimmed with the button by the plugin's own disabled opacity. */\n [data-slot=\"conversation.composer.bar\"] [data-vision-router-mode-toggle]::before {\n content: '';\n width: 16px;\n height: 16px;\n flex: none;\n background: currentColor;\n -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E\");\n mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E\");\n -webkit-mask-size: contain;\n mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n mask-repeat: no-repeat;\n }\n [data-slot=\"conversation.composer.bar\"] [data-vision-router-mode-toggle]:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);\n outline-offset: 2px;\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":"AAgBA,eAAO,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"compat.css.d.ts","sourceRoot":"","sources":["../../../../src/client/styles/compat.css.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,UAAU,44gDAi7BtB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-zen-remote",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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",
|
|
@@ -874,4 +874,89 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
|
|
|
874
874
|
mask-repeat: no-repeat;
|
|
875
875
|
}
|
|
876
876
|
}
|
|
877
|
+
|
|
878
|
+
/* ---------- dsh-vision-router: composer vision toggle (2026-08-26) ----------
|
|
879
|
+
THIRD-PARTY COMPAT RULE — dsh-vision-router registers a Vision mode
|
|
880
|
+
toggle into conversation.input.right (lib/client-presentation-boundary-
|
|
881
|
+
main.js, slot id "vision-router-mode-toggle"): a bordered pill with
|
|
882
|
+
THREE text spans (an eye emoji, the localized label, and a check mark
|
|
883
|
+
while active) sized by INLINE styles — minHeight 28, padding 4px 8px,
|
|
884
|
+
fontSize 12, white-space nowrap — roughly 90px of unshrinkable width.
|
|
885
|
+
|
|
886
|
+
The phone composer row is nowrap by design (composer.css.ts section 1)
|
|
887
|
+
and the model seat is its only shrinkable item, so that pill squeezes
|
|
888
|
+
the model name out of the row (user report, 2026-08-26).
|
|
889
|
+
|
|
890
|
+
Phone treatment: a 28x28 round icon button in the attach button's
|
|
891
|
+
language (same size, same neutral fill). All three spans are hidden and
|
|
892
|
+
ONE 16px eye is drawn by a CSS mask on ::before with currentColor — the
|
|
893
|
+
same survives-React-re-render reasoning as the auto-approve shield
|
|
894
|
+
below: the button re-renders on every state flip, so a DOM-injected
|
|
895
|
+
icon would need an observer while generated content does not. The
|
|
896
|
+
accessible name is unaffected: the button ships aria-label + title with
|
|
897
|
+
the full enable/disable copy, and aria-pressed keeps conveying the
|
|
898
|
+
state; visually the plugin's own inline styles still flip border +
|
|
899
|
+
background + color to the brand token while active (only the inactive
|
|
900
|
+
look is restyled to match the row).
|
|
901
|
+
|
|
902
|
+
Presence gate: [data-vision-router-mode-toggle] is rendered by the
|
|
903
|
+
plugin itself (the exact attribute from its createElement call) — with
|
|
904
|
+
the plugin absent the attribute never exists and this whole section is
|
|
905
|
+
inert, not one selector matching (same convention as the
|
|
906
|
+
dsh-better-sidebar rules). !important throughout because the geometry
|
|
907
|
+
it replaces is inline style. Phone only (max-width: 767px): desktop
|
|
908
|
+
keeps the plugin's full pill untouched, and the 768-1023px tablet row
|
|
909
|
+
has the width for it. order:5 restates composer.css.ts's generic
|
|
910
|
+
input.right rule with the same value — kept here so the toggle can
|
|
911
|
+
never fall back to order 0 (far left, before the model) if that
|
|
912
|
+
structural selector ever stops matching. */
|
|
913
|
+
@media (max-width: 767px) {
|
|
914
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] {
|
|
915
|
+
order: 5 !important;
|
|
916
|
+
flex: 0 0 auto !important;
|
|
917
|
+
box-sizing: border-box !important;
|
|
918
|
+
width: 28px !important;
|
|
919
|
+
min-width: 28px !important;
|
|
920
|
+
max-width: 28px !important;
|
|
921
|
+
height: 28px !important;
|
|
922
|
+
min-height: 28px !important;
|
|
923
|
+
padding: 0 !important;
|
|
924
|
+
gap: 0 !important;
|
|
925
|
+
justify-content: center !important;
|
|
926
|
+
border-radius: 999px !important;
|
|
927
|
+
touch-action: manipulation !important;
|
|
928
|
+
-webkit-tap-highlight-color: transparent;
|
|
929
|
+
}
|
|
930
|
+
/* Inactive look: neutral round fill like the attach button — the
|
|
931
|
+
plugin's own inactive state would draw a lonely outline circle that
|
|
932
|
+
reads as broken next to the row's filled icon buttons. */
|
|
933
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:not([aria-pressed="true"]) {
|
|
934
|
+
border-color: transparent !important;
|
|
935
|
+
background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
|
|
936
|
+
box-shadow: none !important;
|
|
937
|
+
}
|
|
938
|
+
/* Eye emoji + label + active check: all three spans out. */
|
|
939
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > span {
|
|
940
|
+
display: none !important;
|
|
941
|
+
}
|
|
942
|
+
/* The one svg: eye outline + iris as a currentColor mask — brand while
|
|
943
|
+
active, dimmed with the button by the plugin's own disabled opacity. */
|
|
944
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]::before {
|
|
945
|
+
content: '';
|
|
946
|
+
width: 16px;
|
|
947
|
+
height: 16px;
|
|
948
|
+
flex: none;
|
|
949
|
+
background: currentColor;
|
|
950
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E");
|
|
951
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 8C3 5.2 5.3 3.5 8 3.5s5 1.7 6.5 4.5c-1.5 2.8-3.8 4.5-6.5 4.5S3 10.8 1.5 8Z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3Ccircle cx='8' cy='8' r='2'/%3E%3C/svg%3E");
|
|
952
|
+
-webkit-mask-size: contain;
|
|
953
|
+
mask-size: contain;
|
|
954
|
+
-webkit-mask-repeat: no-repeat;
|
|
955
|
+
mask-repeat: no-repeat;
|
|
956
|
+
}
|
|
957
|
+
[data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:focus-visible {
|
|
958
|
+
outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
|
|
959
|
+
outline-offset: 2px;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
877
962
|
`
|