dsh-zen-remote 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +51 -0
  2. package/LICENSE +38 -0
  3. package/README.md +237 -0
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +17 -0
  10. package/cordis.patch.yml.example +16 -0
  11. package/docs/interface.md +200 -0
  12. package/docs/remote-access.en.md +335 -0
  13. package/docs/remote-access.md +422 -0
  14. package/dsh-push.mjs +213 -0
  15. package/lan-gate.mjs +78 -0
  16. package/lib/client.js +6615 -0
  17. package/lib/index.js +310 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/lan-gate-server.cjs +626 -0
  20. package/lib/types/client/MobileAttachButton.d.ts +25 -0
  21. package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
  22. package/lib/types/client/MobileAttachChips.d.ts +32 -0
  23. package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
  24. package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
  25. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
  26. package/lib/types/client/MobileHome.d.ts +24 -0
  27. package/lib/types/client/MobileHome.d.ts.map +1 -0
  28. package/lib/types/client/MobileHomeChips.d.ts +76 -0
  29. package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
  30. package/lib/types/client/MobileNavOverlay.d.ts +15 -0
  31. package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
  32. package/lib/types/client/MobileNavToggle.d.ts +18 -0
  33. package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
  34. package/lib/types/client/MobileSessionHeader.d.ts +58 -0
  35. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
  36. package/lib/types/client/MobileSessionInfo.d.ts +41 -0
  37. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
  38. package/lib/types/client/attach-upload.d.ts +78 -0
  39. package/lib/types/client/attach-upload.d.ts.map +1 -0
  40. package/lib/types/client/chips-store.d.ts +13 -0
  41. package/lib/types/client/chips-store.d.ts.map +1 -0
  42. package/lib/types/client/debug.d.ts +9 -0
  43. package/lib/types/client/debug.d.ts.map +1 -0
  44. package/lib/types/client/effects/aionui-compat.d.ts +4 -0
  45. package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
  46. package/lib/types/client/effects/gestures.d.ts +7 -0
  47. package/lib/types/client/effects/gestures.d.ts.map +1 -0
  48. package/lib/types/client/effects/header-status.d.ts +15 -0
  49. package/lib/types/client/effects/header-status.d.ts.map +1 -0
  50. package/lib/types/client/effects/phone-chrome.d.ts +106 -0
  51. package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
  52. package/lib/types/client/effects/turn-fold.d.ts +25 -0
  53. package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +18 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/locales.d.ts +58 -0
  57. package/lib/types/client/locales.d.ts.map +1 -0
  58. package/lib/types/client/nav-store.d.ts +73 -0
  59. package/lib/types/client/nav-store.d.ts.map +1 -0
  60. package/lib/types/client/session-dot.d.ts +12 -0
  61. package/lib/types/client/session-dot.d.ts.map +1 -0
  62. package/lib/types/client/styles/base.css.d.ts +2 -0
  63. package/lib/types/client/styles/base.css.d.ts.map +1 -0
  64. package/lib/types/client/styles/chips.css.d.ts +2 -0
  65. package/lib/types/client/styles/chips.css.d.ts.map +1 -0
  66. package/lib/types/client/styles/compat.css.d.ts +2 -0
  67. package/lib/types/client/styles/compat.css.d.ts.map +1 -0
  68. package/lib/types/client/styles/composer.css.d.ts +2 -0
  69. package/lib/types/client/styles/composer.css.d.ts.map +1 -0
  70. package/lib/types/client/styles/header.css.d.ts +2 -0
  71. package/lib/types/client/styles/header.css.d.ts.map +1 -0
  72. package/lib/types/client/styles/home.css.d.ts +2 -0
  73. package/lib/types/client/styles/home.css.d.ts.map +1 -0
  74. package/lib/types/client/styles/index.d.ts +15 -0
  75. package/lib/types/client/styles/index.d.ts.map +1 -0
  76. package/lib/types/client/styles/info.css.d.ts +2 -0
  77. package/lib/types/client/styles/info.css.d.ts.map +1 -0
  78. package/lib/types/client/styles/layout.css.d.ts +2 -0
  79. package/lib/types/client/styles/layout.css.d.ts.map +1 -0
  80. package/lib/types/client/styles/misc.css.d.ts +2 -0
  81. package/lib/types/client/styles/misc.css.d.ts.map +1 -0
  82. package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
  83. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
  84. package/lib/types/index.d.ts +83 -0
  85. package/lib/types/index.d.ts.map +1 -0
  86. package/llms.txt +20 -0
  87. package/package.json +128 -0
  88. package/pwa/app.css +95 -0
  89. package/pwa/icons/icon-192.png +0 -0
  90. package/pwa/icons/icon-512.png +0 -0
  91. package/pwa/icons/icon-maskable-512.png +0 -0
  92. package/pwa/icons/icon-maskable.svg +1 -0
  93. package/pwa/icons/icon.svg +1 -0
  94. package/pwa/inject.js +190 -0
  95. package/pwa/manifest.json +29 -0
  96. package/pwa/offline.html +38 -0
  97. package/pwa/sw.js +176 -0
  98. package/pwa/touch-gestures.js +94 -0
  99. package/scripts/build-client.mjs +101 -0
  100. package/scripts/cdp-probe.mjs +64 -0
  101. package/scripts/check-attach-upload.mjs +99 -0
  102. package/scripts/check-sunk-viewport.mjs +33 -0
  103. package/scripts/check-upload-endpoint.mjs +168 -0
  104. package/src/client/MobileAttachButton.tsx +125 -0
  105. package/src/client/MobileAttachChips.tsx +85 -0
  106. package/src/client/MobileDrawerFooter.tsx +56 -0
  107. package/src/client/MobileHome.tsx +348 -0
  108. package/src/client/MobileHomeChips.tsx +426 -0
  109. package/src/client/MobileNavOverlay.tsx +279 -0
  110. package/src/client/MobileNavToggle.tsx +52 -0
  111. package/src/client/MobileSessionHeader.tsx +242 -0
  112. package/src/client/MobileSessionInfo.tsx +303 -0
  113. package/src/client/attach-upload.ts +140 -0
  114. package/src/client/chips-store.ts +41 -0
  115. package/src/client/debug.ts +253 -0
  116. package/src/client/effects/aionui-compat.ts +183 -0
  117. package/src/client/effects/gestures.ts +265 -0
  118. package/src/client/effects/header-status.ts +30 -0
  119. package/src/client/effects/phone-chrome.ts +265 -0
  120. package/src/client/effects/turn-fold.ts +258 -0
  121. package/src/client/index.tsx +227 -0
  122. package/src/client/locales.ts +109 -0
  123. package/src/client/nav-store.ts +78 -0
  124. package/src/client/session-dot.ts +17 -0
  125. package/src/client/styles/base.css.ts +167 -0
  126. package/src/client/styles/chips.css.ts +224 -0
  127. package/src/client/styles/compat.css.ts +681 -0
  128. package/src/client/styles/composer.css.ts +504 -0
  129. package/src/client/styles/header.css.ts +368 -0
  130. package/src/client/styles/home.css.ts +477 -0
  131. package/src/client/styles/index.ts +25 -0
  132. package/src/client/styles/info.css.ts +238 -0
  133. package/src/client/styles/layout.css.ts +501 -0
  134. package/src/client/styles/misc.css.ts +128 -0
  135. package/src/client/styles/turn-fold.css.ts +106 -0
  136. package/src/index.ts +343 -0
@@ -0,0 +1,501 @@
1
+ // layout — split from src/client/mobile.css.ts (2026-08-16), order preserved.
2
+ // Do not reorder: styles/index.ts concatenates in this exact order.
3
+
4
+ export const LAYOUT_CSS = `/* ---------- mobile-only layout ---------- */
5
+
6
+ @media (max-width: 1023px) {
7
+ /* --- Phone chrome ---
8
+ The system status bar stays visible (no fullscreen). Two adjustments
9
+ make it behave:
10
+ - touch-action: manipulation kills double-tap-to-zoom (and the 300ms
11
+ tap delay) while keeping pan and pinch zoom; the client also
12
+ suppresses legacy-iOS gesturestart as a fallback.
13
+ - With the client's viewport-fit=cover, env(safe-area-inset-top) is the
14
+ status bar / notch height; the rules below push the app content below
15
+ it so the status bar never covers anything. Off notched phones (or in
16
+ a normal browser tab where the layout viewport already sits below the
17
+ status bar) the inset is 0 and nothing shifts. */
18
+ html,
19
+ body {
20
+ touch-action: manipulation !important;
21
+ }
22
+
23
+ /* AppFrame: the drawer takes the sidebar column out of grid flow, so the
24
+ remaining in-flow items (center, details) land in tracks 1..2: give the
25
+ center every pixel and keep the details track at zero. The top padding
26
+ clears the status bar / notch for every in-flow surface (session header,
27
+ messages, composer); the absolutely-positioned drawer is unaffected (its
28
+ containing block is the frame's padding box, i.e. still the frame top). */
29
+ /* box-sizing: border-box is load-bearing (S2.1, 2026-08-17). The official
30
+ frame carries height: 100% with the default content-box, so the
31
+ safe-area padding ADDED 54px to its height instead of taking 54px out
32
+ of the content area: the frame grew past the viewport, the document
33
+ became scrollable by exactly the inset, and the very first scroll put
34
+ the header right back under the notch — measured with
35
+ ?mobile-nav-inset=54 (frame 898px tall inside an 844px root, html at
36
+ y=-54). Invisible on every desktop browser because the inset is 0
37
+ there, which is how it shipped. */
38
+ [data-mobile-nav="frame"] {
39
+ position: relative !important;
40
+ box-sizing: border-box !important;
41
+ grid-template-columns: minmax(0, 1fr) 0 0 !important;
42
+ padding-top: var(--mnav-sat) !important;
43
+ }
44
+
45
+ /* The sidebar column (first grid child) becomes a left drawer. The drawer
46
+ hugs the sidebar content exactly (the wide sidebar carries an inline
47
+ width, ~280px): a fixed 92vw box would leave a white strip where the
48
+ container background shows beside the content.
49
+ Closed state: translateX(-110%) — more than -100% of the max-content
50
+ width — guarantees the whole drawer (and its shadow, had it one) leaves
51
+ the viewport. A mere -100% leaves a sliver on screen; -105% (as used
52
+ before) left 14px of the drawer plus a long 32px-blur shadow gradient
53
+ visible along the left edge of the main UI. No box-shadow at all: the
54
+ dimmed backdrop already separates drawer from content. */
55
+ [data-mobile-nav="frame"] > :first-child {
56
+ position: absolute !important;
57
+ inset: 0 auto 0 0 !important;
58
+ width: max-content !important;
59
+ max-width: 92vw !important;
60
+ z-index: 40 !important;
61
+ transform: translateX(-110%);
62
+ transition: transform .28s var(--ds-ease-in-out, ease-in-out);
63
+ background: var(--dsw-alias-bg-base, #ffffff);
64
+ /* Keep the drawer's own content below the status bar / notch: the drawer
65
+ spans the full frame height (its absolute containing block is the
66
+ frame's padding box, so the frame's own safe-area padding does NOT
67
+ reach it). The drawer background paints the status-bar strip, which
68
+ the client's theme-color meta matches, so the strip reads seamless. */
69
+ padding-top: var(--mnav-sat) !important;
70
+ /* Kill the official sidebarCol right border: with the backdrop the edge
71
+ reads cleanly, and the settings dialog (width:100% of this box) stays
72
+ pixel-flush with the drawer. */
73
+ border-right: none !important;
74
+ }
75
+
76
+ /* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
77
+ The open state must be transform:none — NOT translateX(0): an identity
78
+ transform still makes the drawer the containing block for fixed-position
79
+ descendants (the settings dialog's .VOzbGW_overlay is portaled into the
80
+ sidebar DOM). With the identity transform the wide settings sheet
81
+ (100vw-16) overflows the 280px drawer, the dialog's focus scrolls the
82
+ overflow:hidden drawer to scrollLeft=102, and every static child (plus the
83
+ fixed overlay) shifts 102px off-screen. With transform:none the overlay is
84
+ viewport-anchored: it dims the full screen and the sheet sits at left:8. */
85
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child {
86
+ transform: none !important;
87
+ }
88
+
89
+ /* Drag handles are useless on touch and would float over the drawer. */
90
+ [data-side="sidebar"],
91
+ [data-side="details"] {
92
+ display: none !important;
93
+ }
94
+
95
+ /* --- Conversation text on mobile ---
96
+ The official message flow keeps desktop's 32px side gutters and 16px
97
+ type. On a phone: shrink the type a notch and widen the lines by
98
+ trimming the gutters (the sidebar drawer list keeps its size). The
99
+ flow's scroll container is the only _scroll element holding markdown
100
+ <p> paragraphs — the composer's own scroll (textarea) is excluded
101
+ via :has(p). */
102
+ /* The official main scroll body reserves scrollbar-gutter for desktop
103
+ scrollbars (8px), which shoves every column off-center on a phone.
104
+ Classic desktop scrollbars (Edge/Chrome) also occupy ~8-17px in a
105
+ phone-sized viewport, shifting the column further. Mobile scrolling
106
+ is touch/wheel, so remove the scrollbar entirely on phones: the
107
+ column is then exactly centered in every browser. */
108
+ [data-phase] [class$="_scrollBody"] {
109
+ scrollbar-gutter: auto !important;
110
+ scrollbar-width: none !important;
111
+ }
112
+ [data-phase] [class$="_scrollBody"]::-webkit-scrollbar {
113
+ display: none !important;
114
+ width: 0 !important;
115
+ height: 0 !important;
116
+ }
117
+ /* Message action rows (copy / run-time badges) can overflow the right
118
+ edge on narrow screens — keep them inside the message width. */
119
+ [data-phase] [class$="_actions"] {
120
+ overflow: hidden !important;
121
+ }
122
+ [data-phase] [class$="_actions"] [class$="_timeEnd"] {
123
+ flex: 0 1 auto !important;
124
+ min-width: 0 !important;
125
+ overflow: hidden !important;
126
+ text-overflow: ellipsis !important;
127
+ white-space: nowrap !important;
128
+ }
129
+
130
+ [data-phase] [class$="_scroll"]:has(p) {
131
+ padding-left: 20px !important;
132
+ padding-right: 20px !important;
133
+ font-size: 15px !important;
134
+ }
135
+ /* The official markdown styles set an explicit 16px on paragraphs and
136
+ list items, so the container's inherited 15px is not enough. User
137
+ messages render their text in a div whose class carries _text_
138
+ (16px too) — cover it as well. */
139
+ [data-phase] [class$="_scroll"]:has(p) p,
140
+ [data-phase] [class$="_scroll"]:has(p) li,
141
+ [data-phase] [class$="_scroll"]:has(p) [class*="_text_"] {
142
+ font-size: 15px !important;
143
+ }
144
+
145
+ /* Markdown tables: the official table uses width:max-content, so on a phone
146
+ it hugs the content and leaves dead space beside/inside the table. Force
147
+ the table to fill the message column and let the table wrapper handle
148
+ overflow if a cell is genuinely too wide. */
149
+ [data-phase] table {
150
+ width: 100% !important;
151
+ max-width: 100% !important;
152
+ }
153
+ [data-phase] th,
154
+ [data-phase] td {
155
+ max-width: none !important;
156
+ min-width: 0 !important;
157
+ }
158
+
159
+ /* User bubbles: the official stack is capped at min(525px, 82%), which on a
160
+ phone leaves a large blank strip on the left and pushes the bubble high.
161
+ On mobile let the user message fill the same full width as assistant
162
+ messages (the bubble background then spans the whole message column). */
163
+ [data-phase] [class$="_userStack"],
164
+ [data-phase] [class$="_userStack"] [class$="_bubble"] {
165
+ box-sizing: border-box !important;
166
+ width: fit-content !important;
167
+ max-width: 100% !important;
168
+ }
169
+
170
+ /* --- Composer bottom row on mobile ---
171
+ The official row gives the model pill (trailing) flex:0 0 auto, which
172
+ squeezes the agent-permission pill (modes) down to 15px: the pill's
173
+ chevron then overflows on top of the model name. Let the permission
174
+ pill keep its natural width and let the model pill shrink instead.
175
+ Anchored by the composer card (:has(textarea)): row = the _row class
176
+ containing a _trailing group, tools = its first child, permission pill
177
+ = its 2nd child, model pill = the _trailing group.
178
+ NOTE: do NOT anchor these by the card's :last-child — a client effect
179
+ moves the git-graph branch chip INTO the card, which becomes the card's
180
+ new last child and silently disables every :last-child rule (the model
181
+ pill then falls back to the official fixed layout and long model IDs
182
+ overflow instead of ellipsizing). Structural anchors only. */
183
+ /* The official row pads every group with 12px gaps, which on a phone eats
184
+ width the model pill needs for the full model ID. Tighten the row to 8px
185
+ gaps (the modes pill keeps its natural width) so the model name gets
186
+ every spare pixel. */
187
+ [data-phase] [class*="_card"]:has(textarea) [class$="_row"]:has([class$="_trailing"]) {
188
+ gap: 8px !important;
189
+ }
190
+ [data-phase] [class*="_card"]:has(textarea) [class$="_row"]:has([class$="_trailing"]) > :first-child {
191
+ gap: 8px !important;
192
+ }
193
+ [data-phase] [class*="_card"]:has(textarea) [class$="_row"]:has([class$="_trailing"]) > :first-child > :nth-child(2) {
194
+ flex: 0 0 auto !important;
195
+ gap: 8px !important;
196
+ }
197
+ [data-phase] [class*="_card"]:has(textarea) [class$="_trailing"] {
198
+ flex: 1 1 auto !important;
199
+ gap: 8px !important;
200
+ min-width: 0 !important;
201
+ }
202
+ /* Let the model selector take the free space in the trailing group so the
203
+ model name is not squeezed/truncated; the context meter stays fixed. */
204
+ [data-phase] [class*="_card"]:has(textarea) [class$="_root"]:has(> [class$="_trigger"][aria-haspopup="menu"]) {
205
+ flex: 1 1 auto !important;
206
+ min-width: 0 !important;
207
+ }
208
+ [data-phase] [class*="_card"]:has(textarea) [class$="_root"]:has(> [class$="_trigger"][aria-haspopup="menu"]) > [class$="_trigger"] {
209
+ width: 100% !important;
210
+ max-width: 100% !important;
211
+ }
212
+ /* The model label must absorb the trigger's free width: officially it is a
213
+ fixed-content flex item, so the spare width of a grown pill sits unused
214
+ between the chevron and the pill edge. Let the label grow and shrink:
215
+ the full model ID shows whenever the row can fit it, and the ellipsis
216
+ lands exactly at the available width otherwise. */
217
+ [data-phase] [class*="_card"]:has(textarea) [class$="_root"]:has(> [class$="_trigger"][aria-haspopup="menu"]) > [class$="_trigger"] > [class$="_triggerLabel"] {
218
+ flex: 1 1 auto !important;
219
+ min-width: 0 !important;
220
+ }
221
+ [data-phase] [class*="_card"]:has(textarea) [class$="_root"]:has(> [class$="_trigger"]:not([aria-haspopup="menu"])) {
222
+ flex: 0 0 auto !important;
223
+ }
224
+
225
+ /* Model switcher menu: the official dropdown is right-aligned to the
226
+ trigger (right:0). The model pill sits near the center of the composer,
227
+ so on a phone the 240px menu overflows the left edge and looks off-center.
228
+ Center the menu on the trigger instead. */
229
+ [data-phase] [class*="_card"]:has(textarea) [class$="_root"]:has(> [class$="_trigger"]) > [class$="_menu"] {
230
+ left: 50% !important;
231
+ right: auto !important;
232
+ transform: translateX(-50%) !important;
233
+ }
234
+
235
+ /* --- Session header on mobile ---
236
+ Layout goal: [toggle] [session title] [mode badge] in a row, with the
237
+ Session log capsule removed from the header (relocated to the drawer
238
+ footer). Stable structural hooks only:
239
+ [data-phase] header the session header element
240
+ header > :first-child titleRow (titleCluster + utilities)
241
+ header > :first-child > :last-child headerUtilities (Session log seat) */
242
+ [data-phase] header {
243
+ padding-right: 12px !important;
244
+ }
245
+ /* Give the title row a lane clear of the absolutely-placed toggle, then
246
+ balance the header: with header padding-right 12px, a 20px left
247
+ padding puts the title's geometric center exactly on the viewport
248
+ center (measured 195/195 at 390px). */
249
+ [data-phase] header > :first-child {
250
+ padding-left: 20px !important;
251
+ }
252
+ /* The directory toggle sits at the far left of the header (the header
253
+ is position:relative; the data-slot wrappers are display:contents). */
254
+ [data-mobile-nav="toggle"] {
255
+ position: absolute !important;
256
+ left: 8px !important;
257
+ top: 12px !important;
258
+ z-index: 2 !important;
259
+ }
260
+ /* The Files action stays in-flow inside headerActions. On mobile headerActions
261
+ does not expand to fill the row: it is pushed to the right edge and keeps
262
+ its content size, so [mode] [subagent/jobs] [files] sit close together
263
+ with the normal 8px gaps — compact, no artificial blank columns. Explicit
264
+ flex order keeps Files rightmost regardless of DOM order. */
265
+ [data-mobile-nav="files"] {
266
+ position: static !important;
267
+ left: auto !important;
268
+ right: auto !important;
269
+ top: auto !important;
270
+ z-index: auto !important;
271
+ }
272
+ [data-phase] header [class$="_headerActions"] {
273
+ flex: 0 1 auto !important;
274
+ min-width: 0 !important;
275
+ margin-left: auto !important;
276
+ justify-content: flex-end !important;
277
+ }
278
+ /* The session title/crumb must yield space on mobile so the mode label and
279
+ subagent cluster are not squeezed to a single letter. Cap it at ~24vw and
280
+ let it truncate with ellipsis. */
281
+ [data-phase] header [class$="_crumbs"] {
282
+ flex: 0 1 auto !important;
283
+ min-width: 0 !important;
284
+ max-width: 24vw !important;
285
+ }
286
+ /* Mode label (AgentPresetLabel): the official pill uses inline-flex, whose
287
+ direct text node is only clipped — no ellipsis. Convert the pill to a
288
+ block container, keep its icon absolutely positioned on the left, and let
289
+ it size naturally up to 42vw so the mode name stays readable while the
290
+ subagent/Files cluster still fits on a phone. */
291
+ [data-phase] header [class$="_label"]:has(> svg) {
292
+ order: 1 !important;
293
+ flex: 0 1 auto !important;
294
+ min-width: 0 !important;
295
+ max-width: 42vw !important;
296
+ display: block !important;
297
+ position: relative !important;
298
+ box-sizing: border-box !important;
299
+ padding-left: 18px !important;
300
+ padding-right: 2px !important;
301
+ overflow: hidden !important;
302
+ text-overflow: ellipsis !important;
303
+ white-space: nowrap !important;
304
+ }
305
+ [data-phase] header [class$="_label"]:has(> svg) > svg {
306
+ position: absolute !important;
307
+ left: 0 !important;
308
+ top: 50% !important;
309
+ transform: translateY(-50%) !important;
310
+ }
311
+ /* Subagent/jobs helpers: keep their triggers on one line and never let the
312
+ row grow artificial empty space. */
313
+ [data-phase] header [class$="_root"]:has(> button[class$="_trigger"]) {
314
+ order: 2 !important;
315
+ flex: 0 0 auto !important;
316
+ min-width: max-content !important;
317
+ max-width: none !important;
318
+ white-space: nowrap !important;
319
+ /* On mobile the popover is anchored to the header (viewport-width
320
+ containing block) instead of the trigger, so a small left inset keeps it
321
+ fully on screen and never clips the tree's status dots. */
322
+ position: static !important;
323
+ }
324
+ [data-phase] header [class$="_root"]:has(> button[class$="_trigger"]) > button,
325
+ [data-phase] header [class$="_root"]:has(> button[class$="_trigger"]) > button * {
326
+ white-space: nowrap !important;
327
+ }
328
+ [data-phase] header [data-mobile-nav="files"] {
329
+ order: 3 !important;
330
+ flex: 0 0 auto !important;
331
+ }
332
+ /* Session log download: gone from the header row on mobile (the utilities
333
+ seat holds only the session-log-export capsule). */
334
+ [data-phase] header > :first-child > :last-child {
335
+ display: none !important;
336
+ }
337
+
338
+ /* --- Header popovers on mobile (dsh-client-ui-jobs / dsh-client-ui-subagent) ---
339
+ The official entries sit in the session header actions. Their popovers
340
+ are anchored to the trigger's LEFT edge (left: 0), so from a right-edge
341
+ trigger the 336px panel spills past the viewport. Re-anchor them to the
342
+ trigger's right edge instead. The popover classes are hashed CSS-module
343
+ names, so target them by the stable _menu suffix inside the header. */
344
+ /* Header popovers on mobile: anchor to the left edge with a small inset and
345
+ clamp the width, instead of right-aligning to a trigger near the right
346
+ edge (which pushed the panel left off-screen and clipped the status dots).
347
+ Also cap the height so the panel does not run into the composer/task bar. */
348
+ [data-phase] header [class$="_menu"] {
349
+ left: 8px !important;
350
+ right: auto !important;
351
+ width: min(336px, calc(100vw - 16px)) !important;
352
+ max-width: none !important;
353
+ max-height: min(420px, calc(100dvh - 120px)) !important;
354
+ }
355
+ /* --- Settings dialog on mobile ---
356
+ Desktop: 800px two-column flex (188px nav + content). Mobile: a
357
+ near-full-width sheet — nav tabs wrap into rows on top, option rows
358
+ stay horizontal (title+description left, control right). Structural
359
+ selectors are scoped to the unique aria-modal dialog; every
360
+ settings-specific rule is gated with
361
+ :has(> :first-child > :last-child > button) — the settings nav tab
362
+ list holds <button> tabs, so the transient export dialog (the same
363
+ primitives Modal, header(title+close)+description+body) keeps its
364
+ official centered card layout. Requires :has() support
365
+ (Chromium 105+, 2022).
366
+
367
+ The directory picker (dsh-client-ui-directory-picker-browse) must be
368
+ excluded too: its footer bar holds <button> children AND its breadcrumb
369
+ trail (role="navigation") — which the role gate relies on to exclude
370
+ it — is REPLACED by the path input in edit mode (pencil button), so
371
+ without the ZuhsRW exclusion clicking the pencil would suddenly match
372
+ this sheet rule: the dialog jumps to the top of the screen, the header
373
+ (with the path input) is hidden by the > :first-child > :first-child
374
+ display:none rule below, and the user can no longer type a path
375
+ (issue #12, 2026-08-16). The picker family keeps the official layout
376
+ on mobile in every mode. */
377
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) {
378
+ position: absolute !important;
379
+ left: 8px !important;
380
+ /* Fixed top (no translateY): a transform on the panel combined with the
381
+ panel overflowing the max-content drawer shifts the fixed overlay's
382
+ coordinate frame, dragging the whole sidebar content off-screen. The
383
+ safe-area inset keeps the sheet below the status bar / notch. */
384
+ top: calc(var(--mnav-sat) + 12px) !important;
385
+ width: calc(100vw - 16px) !important;
386
+ max-width: calc(100vw - 16px) !important;
387
+ /* Height follows the content (no dead space under a short page); it
388
+ caps at 100dvh-24 (less the safe-area top) and the options area
389
+ scrolls only then. */
390
+ height: auto !important;
391
+ max-height: min(800px, calc(100vh - 24px - var(--mnav-sat))) !important;
392
+ max-height: min(800px, calc(100dvh - 24px - var(--mnav-sat))) !important;
393
+ flex-direction: column !important;
394
+ border-radius: 14px !important;
395
+ animation: dsh-mobile-nav-sheet-in .22s var(--ds-ease-out, ease-in-out);
396
+ }
397
+ /* The settings sheet's dimmed mask fades in with the panel (the mask is
398
+ the first child of the overlay that directly contains the sheet). */
399
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"]))) > :first-child {
400
+ animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);
401
+ }
402
+ @media (prefers-reduced-motion: reduce) {
403
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])),
404
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"]))) > :first-child {
405
+ animation: none !important;
406
+ }
407
+ }
408
+ /* The export dialog (not the settings sheet) must never overflow the
409
+ viewport: the official centered card can be wider than 390px.
410
+ :not([data-mobile-nav="info-sheet"]) (S4, 2026-08-17): the session-info
411
+ sheet also carries role="dialog" aria-modal="true" (correct a11y
412
+ semantics for a bottom sheet with a scrim) and has no button as its
413
+ first-child's last-child, so without this exclusion it silently matched
414
+ this generic selector too — measured 358px (100vw-32px) instead of the
415
+ 374px its own left:8px/right:8px margins specify. Same category of bug
416
+ as the ZuhsRW directory-picker collision below; same fix shape. */
417
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)):not([data-mobile-nav="info-sheet"]) {
418
+ max-width: calc(100vw - 32px) !important;
419
+ }
420
+ /* Nav bar: hide the "Settings" caption (redundant on a full-width sheet)
421
+ and wrap the tab list so every tab is visible — a horizontal scroll cut
422
+ the last tab ("Plugins") off with no affordance to scroll. */
423
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child {
424
+ width: 100% !important;
425
+ flex-direction: row !important;
426
+ align-items: center !important;
427
+ gap: 6px !important;
428
+ padding: 10px 12px 8px !important;
429
+ }
430
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child > :first-child {
431
+ display: none !important;
432
+ }
433
+ /* The tab list scrolls in the space left by the toolbar: the toolbar
434
+ (config file + close) is reparented INTO this nav row by a client
435
+ effect (MobileNavOverlay), so the tab list must be anchored by its
436
+ class, NOT by :last-child (the reparented toolbar becomes the nav's
437
+ new last child). */
438
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child [class$="_navList"] {
439
+ flex: 1 1 auto !important;
440
+ min-width: 0 !important;
441
+ flex-direction: row !important;
442
+ flex-wrap: wrap !important;
443
+ gap: 6px !important;
444
+ overflow: visible !important;
445
+ }
446
+ /* Content toolbar (Open configuration file + close): grouped flush to
447
+ the right edge, and reparented INTO the nav row on mobile so it shares
448
+ one line with the tabs (user feedback 2026-08-16 — the toolbar's own
449
+ row left a full-width dead gap under the tabs). Anchored by class: the
450
+ header leaves the content subtree, so :first-child/:last-child anchors
451
+ would now hit the options area. Children carry official auto-margins
452
+ that would defeat flex-end, so neutralize them. The close button gets
453
+ a round tappable base so it reads as its own control, not part of the
454
+ outline button. */
455
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) [class$="_header"] {
456
+ flex: 0 0 auto !important;
457
+ justify-content: flex-end !important;
458
+ align-items: center !important;
459
+ gap: 8px !important;
460
+ padding: 0 0 0 4px !important;
461
+ min-height: 40px !important;
462
+ }
463
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) [class$="_header"] > * {
464
+ margin-left: 0 !important;
465
+ margin-right: 0 !important;
466
+ }
467
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) [class$="_header"] > :last-child {
468
+ width: 32px !important;
469
+ height: 32px !important;
470
+ border-radius: 50% !important;
471
+ display: inline-flex !important;
472
+ align-items: center !important;
473
+ justify-content: center !important;
474
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
475
+ }
476
+ /* Appearance mode cards: the official cube row renders three tall
477
+ vertical cards (~268px) that eat half the sheet. Turn them into a
478
+ compact horizontal trio (icon + label inline, equal widths).
479
+ Relies on the official cube-row class name of this version. */
480
+ [aria-modal="true"] [class$="_cubeRow"] {
481
+ gap: 6px !important;
482
+ }
483
+ [aria-modal="true"] [class$="_cubeRow"] > * {
484
+ flex: 1 1 0 !important;
485
+ flex-direction: row !important;
486
+ align-items: center !important;
487
+ justify-content: center !important;
488
+ gap: 6px !important;
489
+ padding: 10px 8px !important;
490
+ min-height: 0 !important;
491
+ }
492
+ /* Content: the options scroll area gets bottom breathing room so the last
493
+ row never sits flush against the sheet's rounded corner. */
494
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :last-child {
495
+ flex: 1 1 auto !important;
496
+ min-height: 0 !important;
497
+ }
498
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :last-child > :last-child {
499
+ padding: 0 12px 24px !important;
500
+ }
501
+ `
@@ -0,0 +1,128 @@
1
+ // misc — split from src/client/mobile.css.ts (2026-08-16), order preserved.
2
+ // Do not reorder: styles/index.ts concatenates in this exact order.
3
+
4
+ export const MISC_CSS = ` /* ---------- hero composer on mobile ----------
5
+ The official hero card carries a 2-line textarea plus a tall tool row,
6
+ which reads oversized on a phone. Tighten the empty-state rhythm: keep
7
+ the official centered hero, shrink the textarea line box, slim the card
8
+ padding and the tool row, and close the gap under the headline. */
9
+
10
+ [data-phase="hero"] [class$="_card"]:has(textarea) {
11
+ padding-top: 6px !important;
12
+ gap: 8px !important;
13
+ }
14
+ /* The official composer autosizes the textarea and writes an inline
15
+ height (2 lines on the hero empty state) on the textarea's scroll/grow
16
+ wrappers. :placeholder-shown lets us collapse the EMPTY state to one
17
+ line with !important; as soon as the user types, the pseudo-class no
18
+ longer matches and the autosizer's inline height takes over again — so
19
+ multi-line growth keeps working. */
20
+ [data-phase="hero"] textarea:placeholder-shown {
21
+ height: 28px !important;
22
+ }
23
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) > [class$="_scroll"],
24
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) [class$="_grow"] {
25
+ height: 28px !important;
26
+ }
27
+ [data-phase="hero"] [class$="_card"]:has(textarea) > [class$="_row"] {
28
+ padding-top: 2px !important;
29
+ }
30
+ [data-phase="hero"] [class$="_headline"] {
31
+ line-height: 1.15 !important;
32
+ margin-bottom: 0 !important;
33
+ }
34
+ [data-phase="hero"] [class$="_stack"] {
35
+ gap: 0 !important;
36
+ }
37
+
38
+ /* ---------- composer dock: swap git branch chip with the todo card ----------
39
+ The git-graph branch chip (conversation.input.dock, order 100) floats
40
+ alone at the bottom-left above the input card, with a dead zone to its
41
+ right; the full-width todo card (order 0) sits above it. Swap them so
42
+ the chip reads as the stack's top row and the todo card fills the row
43
+ above the composer. The dock container itself is display:contents
44
+ (inline style) — its children are direct flex items of the composer
45
+ stack, so order on the children is what reorders them. Only the chip
46
+ needs an order change: -1 puts it before the todo card (order 0) and
47
+ before the input card (order 0, later in DOM). The todo card must KEEP
48
+ its order 0 — raising it past the input card's order 0 would drop it
49
+ below the composer entirely (2026-08-16 regression, fixed). The queue
50
+ strip (order 20) keeps hugging the input card. Desktop untouched (this
51
+ block lives inside the max-width: 1023px media query). */
52
+ [data-slot="conversation.input.dock"] [data-gitgraph-chip-anchor] {
53
+ order: -1 !important;
54
+ }
55
+ /* Mobile tap target + feedback for the branch chip (git-graph, 24px
56
+ desktop spec). Two real-world problems: ① the chip is tiny and sits
57
+ right above the expandable todo card — mis-taps land on the todo card;
58
+ ② opening the popover waits for the host's /git/branches round-trip
59
+ (~700ms on device) with zero feedback, so users tap again and toggle
60
+ the popover closed. Enlarge the target, kill double-tap zoom delay,
61
+ and give an instant pressed state so a tap reads as registered. */
62
+ [data-slot="conversation.input.dock"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {
63
+ touch-action: manipulation !important;
64
+ min-height: 34px !important;
65
+ padding: 0 12px !important;
66
+ font-size: 13px !important;
67
+ }
68
+ [data-slot="conversation.input.dock"] [data-gitgraph-chip-anchor] [data-gitgraph-chip]:active {
69
+ transform: scale(.96) !important;
70
+ transition: transform .12s !important;
71
+ }
72
+ }
73
+
74
+ /* ---------- tablet / wide mobile: keep sheets from becoming full-width ----------
75
+ Below 768px the near-full-width sheets are the right call for a phone.
76
+ On wider but still sub-desktop viewports (foldables, tablet portrait,
77
+ desktop-mode tall windows) the same full-bleed sheet leaves content
78
+ clustered at the left edge with a large dead zone on the right. Cap and
79
+ center the modal sheets and the aionui bottom sheets instead. */
80
+ @media (min-width: 768px) and (max-width: 1023px) {
81
+ /* All modal dialogs: centered, never edge-to-edge. The settings sheet has
82
+ a higher-specificity full-width rule above, so repeat its selector here
83
+ to win; the generic export/other-modal rule is covered by the second
84
+ selector. */
85
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])),
86
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)) {
87
+ left: 0 !important;
88
+ right: 0 !important;
89
+ margin-left: auto !important;
90
+ margin-right: auto !important;
91
+ width: min(calc(100vw - 32px), 720px) !important;
92
+ max-width: min(calc(100vw - 32px), 720px) !important;
93
+ }
94
+
95
+ /* The dsh-web-ui explorer / preview bottom sheets: same treatment — keep
96
+ the mobile bottom-sheet behavior, but stop them spanning the full width. */
97
+ [data-aionui-explorer-col],
98
+ [data-aionui-preview-col] {
99
+ left: 0 !important;
100
+ right: 0 !important;
101
+ width: min(calc(100vw - 32px), 720px) !important;
102
+ margin-left: auto !important;
103
+ margin-right: auto !important;
104
+ }
105
+
106
+ /* Settings sections (e.g. Agent presets) often carry a desktop max-width
107
+ (720px) that leaves a dead strip on the right once the sheet is capped to
108
+ the same width; let them fill the sheet body instead. */
109
+ [aria-modal="true"] [class$="_section"] {
110
+ width: 100% !important;
111
+ max-width: none !important;
112
+ }
113
+ }
114
+
115
+ /* ---------- desktop: the mobile controls must never appear ---------- */
116
+
117
+ @media (min-width: 1024px) {
118
+ [data-mobile-nav="toggle"],
119
+ [data-mobile-nav="files"],
120
+ [data-mobile-nav="fab"],
121
+ [data-mobile-nav="backdrop"],
122
+ [data-mobile-nav="session-log"],
123
+ [data-mobile-nav="explorer"],
124
+ [data-mobile-nav="drawer-actions"] {
125
+ display: none !important;
126
+ }
127
+ }
128
+ `