dsh-pocket 0.1.0 → 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.
package/client/client.js CHANGED
@@ -30,7 +30,7 @@ __export(index_exports, {
30
30
  redactStatus: () => redactStatus
31
31
  });
32
32
  module.exports = __toCommonJS(index_exports);
33
- var import_react = require("react");
33
+ var import_react2 = require("react");
34
34
 
35
35
  // client/api.js
36
36
  var POCKET_RPC_CHANNEL = "/dsh-pocket";
@@ -52,9 +52,1195 @@ function redactStatus(s) {
52
52
  };
53
53
  }
54
54
 
55
+ // client/mobile/MobileNavToggle.tsx
56
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
57
+ function MobileNavToggle({ toggleSidebar, t }) {
58
+ const toggleExplorer = () => {
59
+ const frame = document.querySelector('[data-mobile-nav="frame"]');
60
+ if (frame === null) return;
61
+ if (frame.hasAttribute("data-aionui-explorer-open")) {
62
+ frame.removeAttribute("data-aionui-explorer-open");
63
+ } else {
64
+ frame.setAttribute("data-aionui-explorer-open", "");
65
+ }
66
+ };
67
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
68
+ "button",
69
+ {
70
+ type: "button",
71
+ "data-mobile-nav": "toggle",
72
+ "aria-label": t("open"),
73
+ title: t("open"),
74
+ onClick: () => toggleSidebar()
75
+ },
76
+ /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconPanelLeftOutline16, { size: 16 })
77
+ ), /* @__PURE__ */ React.createElement(
78
+ "button",
79
+ {
80
+ type: "button",
81
+ "data-mobile-nav": "files",
82
+ "aria-label": t("files"),
83
+ title: t("files"),
84
+ onClick: toggleExplorer
85
+ },
86
+ /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 16 })
87
+ ));
88
+ }
89
+
90
+ // client/mobile/MobileNavOverlay.tsx
91
+ var import_react = require("react");
92
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
93
+ var MOBILE_QUERY = "(max-width: 1023px)";
94
+ function useMobile() {
95
+ const [mobile, setMobile] = (0, import_react.useState)(() => window.matchMedia(MOBILE_QUERY).matches);
96
+ (0, import_react.useEffect)(() => {
97
+ const query = window.matchMedia(MOBILE_QUERY);
98
+ const onChange = (event) => setMobile(event.matches);
99
+ query.addEventListener("change", onChange);
100
+ return () => query.removeEventListener("change", onChange);
101
+ }, []);
102
+ return mobile;
103
+ }
104
+ function findFrame() {
105
+ return document.querySelector("[data-shell-overlay]")?.parentElement ?? null;
106
+ }
107
+ function MobileNavOverlay({ toggleSidebar, t }) {
108
+ const mobile = useMobile();
109
+ const [open, setOpen] = (0, import_react.useState)(false);
110
+ const [fabVisible, setFabVisible] = (0, import_react.useState)(false);
111
+ (0, import_react.useLayoutEffect)(() => {
112
+ if (!mobile) {
113
+ setOpen(false);
114
+ return;
115
+ }
116
+ const frame = findFrame();
117
+ if (frame === null) return;
118
+ frame.setAttribute("data-mobile-nav", "frame");
119
+ const sync = () => setOpen(!frame.hasAttribute("data-sidebar-collapsed"));
120
+ sync();
121
+ const observer = new MutationObserver(sync);
122
+ observer.observe(frame, { attributes: true, attributeFilter: ["data-sidebar-collapsed"] });
123
+ return () => {
124
+ observer.disconnect();
125
+ frame.removeAttribute("data-mobile-nav");
126
+ };
127
+ }, [mobile]);
128
+ (0, import_react.useEffect)(() => {
129
+ if (!mobile) {
130
+ setFabVisible(false);
131
+ return;
132
+ }
133
+ const sync = () => setFabVisible(document.querySelector('[data-phase="active"]') === null);
134
+ sync();
135
+ const observer = new MutationObserver(sync);
136
+ observer.observe(document.documentElement, {
137
+ subtree: true,
138
+ childList: true,
139
+ attributes: true,
140
+ attributeFilter: ["data-phase"]
141
+ });
142
+ return () => observer.disconnect();
143
+ }, [mobile]);
144
+ (0, import_react.useEffect)(() => {
145
+ if (!mobile || !open) return;
146
+ const onKeyDown = (event) => {
147
+ if (event.key === "Escape" && document.querySelector('[aria-modal="true"]') === null) toggleSidebar();
148
+ };
149
+ document.addEventListener("keydown", onKeyDown, true);
150
+ return () => document.removeEventListener("keydown", onKeyDown, true);
151
+ }, [mobile, open, toggleSidebar]);
152
+ (0, import_react.useEffect)(() => {
153
+ if (!mobile || !open) return;
154
+ const onDrawerClick = (event) => {
155
+ if (document.querySelector('[aria-modal="true"]') !== null) return;
156
+ const target = event.target;
157
+ if (target === null) return;
158
+ const drawer = document.querySelector('[data-mobile-nav="frame"] > :first-child');
159
+ if (drawer === null || !drawer.contains(target)) return;
160
+ if (target.closest('[class*="sessionRow"] button') !== null) return;
161
+ const navigates = target.closest(
162
+ 'button[data-dsh-taskboard-entry], button[data-dsh-ssh-entry], [class*="newSession"], [class*="sessionRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"]'
163
+ );
164
+ if (navigates !== null) toggleSidebar();
165
+ };
166
+ document.addEventListener("click", onDrawerClick, true);
167
+ return () => document.removeEventListener("click", onDrawerClick, true);
168
+ }, [mobile, open, toggleSidebar]);
169
+ if (!mobile) return null;
170
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, open && /* @__PURE__ */ React.createElement(
171
+ "div",
172
+ {
173
+ "data-mobile-nav": "backdrop",
174
+ role: "button",
175
+ "aria-label": t("backdrop"),
176
+ onClick: () => toggleSidebar()
177
+ }
178
+ ), fabVisible && !open && /* @__PURE__ */ React.createElement(
179
+ "button",
180
+ {
181
+ type: "button",
182
+ "data-mobile-nav": "fab",
183
+ "aria-label": t("open"),
184
+ title: t("open"),
185
+ onClick: () => toggleSidebar()
186
+ },
187
+ /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives2.IconPanelLeftOutline16, { size: 18 })
188
+ ));
189
+ }
190
+
191
+ // client/mobile/MobileDrawerFooter.tsx
192
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
193
+ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t }) {
194
+ const sessionId = useSessions((state) => state.current);
195
+ const openExplorer = () => {
196
+ document.querySelector('[data-mobile-nav="frame"]')?.setAttribute("data-aionui-explorer-open", "");
197
+ toggleSidebar();
198
+ };
199
+ return /* @__PURE__ */ React.createElement("div", { "data-mobile-nav": "drawer-actions" }, /* @__PURE__ */ React.createElement(
200
+ "button",
201
+ {
202
+ type: "button",
203
+ "data-mobile-nav": "explorer",
204
+ "aria-label": t("files"),
205
+ title: t("files"),
206
+ onClick: openExplorer
207
+ },
208
+ /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives3.IconPanelLeftOutline16, { size: 14 }),
209
+ /* @__PURE__ */ React.createElement("span", null, t("files"))
210
+ ), /* @__PURE__ */ React.createElement(
211
+ "button",
212
+ {
213
+ type: "button",
214
+ "data-mobile-nav": "session-log",
215
+ "aria-label": t("sessionLog"),
216
+ title: t("sessionLog"),
217
+ disabled: sessionId === void 0,
218
+ onClick: () => {
219
+ if (sessionId !== void 0) downloadSessionLog(sessionId);
220
+ }
221
+ },
222
+ /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives3.IconDownloadOutline16, { size: 14 }),
223
+ /* @__PURE__ */ React.createElement("span", null, t("sessionLog"))
224
+ ));
225
+ }
226
+
227
+ // client/mobile/mobile.css.ts
228
+ var MOBILE_CSS = `
229
+ /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */
230
+
231
+ [data-mobile-nav="toggle"],
232
+ [data-mobile-nav="files"] {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ width: 28px;
237
+ height: 28px;
238
+ flex: none;
239
+ padding: 0;
240
+ border: none;
241
+ border-radius: 50%;
242
+ background: transparent;
243
+ color: var(--dsw-alias-label-secondary, inherit);
244
+ cursor: pointer;
245
+ -webkit-tap-highlight-color: transparent;
246
+ }
247
+ [data-mobile-nav="toggle"]:hover,
248
+ [data-mobile-nav="files"]:hover {
249
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
250
+ }
251
+ [data-mobile-nav="toggle"]:focus-visible,
252
+ [data-mobile-nav="files"]:focus-visible {
253
+ outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
254
+ outline-offset: 1px;
255
+ }
256
+
257
+ /* Drawer footer actions: the relocated Session log download plus the Files
258
+ action that opens the dsh-web-ui explorer sheet. */
259
+ [data-mobile-nav="drawer-actions"] {
260
+ display: inline-flex;
261
+ align-items: center;
262
+ gap: 8px;
263
+ }
264
+ [data-mobile-nav="session-log"],
265
+ [data-mobile-nav="explorer"] {
266
+ display: inline-flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ gap: 6px;
270
+ height: 34px;
271
+ padding: 0 12px;
272
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12));
273
+ border-radius: 12px;
274
+ background: transparent;
275
+ color: var(--dsw-alias-label-primary, inherit);
276
+ font-family: inherit;
277
+ font-size: 13px;
278
+ line-height: 20px;
279
+ cursor: pointer;
280
+ -webkit-tap-highlight-color: transparent;
281
+ }
282
+ [data-mobile-nav="session-log"]:hover:not(:disabled),
283
+ [data-mobile-nav="explorer"]:hover {
284
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
285
+ }
286
+ [data-mobile-nav="session-log"]:disabled {
287
+ color: var(--dsw-alias-label-dimmed, rgba(0, 0, 0, .35));
288
+ cursor: default;
289
+ }
290
+
291
+ /* Floating fallback button (hero / blank phases without a session header).
292
+ The top clears the camera band below the status bar; when the client has
293
+ set viewport-fit=cover the safe-area inset moves it below the notch too. */
294
+ [data-mobile-nav="fab"] {
295
+ position: absolute;
296
+ top: calc(env(safe-area-inset-top, 0px) + 72px);
297
+ left: 10px;
298
+ z-index: 21;
299
+ display: inline-flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ width: 38px;
303
+ height: 38px;
304
+ padding: 0;
305
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12));
306
+ border-radius: 50%;
307
+ background: var(--dsw-alias-button-floating-fill, #ffffff);
308
+ color: var(--dsw-alias-label-primary, inherit);
309
+ cursor: pointer;
310
+ box-shadow: 0 2px 12px rgba(0, 0, 0, .18);
311
+ -webkit-tap-highlight-color: transparent;
312
+ }
313
+ [data-mobile-nav="fab"]:hover {
314
+ background: var(--dsw-alias-button-floating-hover, rgba(0, 0, 0, .08));
315
+ }
316
+ [data-mobile-nav="fab"]:focus-visible {
317
+ outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
318
+ outline-offset: 2px;
319
+ }
320
+
321
+ /* Dimmed backdrop under the open drawer; above every column, below the drawer. */
322
+ [data-mobile-nav="backdrop"] {
323
+ position: absolute;
324
+ inset: 0;
325
+ z-index: 30;
326
+ background: rgba(0, 0, 0, .45);
327
+ cursor: pointer;
328
+ animation: dsh-mobile-nav-fade .2s var(--ds-ease-in-out, ease-in-out);
329
+ -webkit-tap-highlight-color: transparent;
330
+ }
331
+ @keyframes dsh-mobile-nav-fade {
332
+ from { opacity: 0; }
333
+ to { opacity: 1; }
334
+ }
335
+ /* Settings sheet entrance: the official dialog mounts with no animation at
336
+ all, so it snaps in. Fade + slight rise/scale reads as a proper sheet. */
337
+ @keyframes dsh-mobile-nav-sheet-in {
338
+ from {
339
+ opacity: 0;
340
+ transform: translateY(14px) scale(.98);
341
+ }
342
+ to {
343
+ opacity: 1;
344
+ transform: none;
345
+ }
346
+ }
347
+ /* Preview sheet rise: the aionui preview column opens as a bottom sheet. */
348
+ @keyframes dsh-mobile-nav-sheet-up {
349
+ from {
350
+ opacity: 0;
351
+ transform: translateY(28px);
352
+ }
353
+ to {
354
+ opacity: 1;
355
+ transform: none;
356
+ }
357
+ }
358
+
359
+ /* ---------- mobile-only layout ---------- */
360
+
361
+ @media (max-width: 1023px) {
362
+ /* --- Phone chrome ---
363
+ The system status bar stays visible (no fullscreen). Two adjustments
364
+ make it behave:
365
+ - touch-action: manipulation kills double-tap-to-zoom (and the 300ms
366
+ tap delay) while keeping pan and pinch zoom; the client also
367
+ suppresses legacy-iOS gesturestart as a fallback.
368
+ - With the client's viewport-fit=cover, env(safe-area-inset-top) is the
369
+ status bar / notch height; the rules below push the app content below
370
+ it so the status bar never covers anything. Off notched phones (or in
371
+ a normal browser tab where the layout viewport already sits below the
372
+ status bar) the inset is 0 and nothing shifts. */
373
+ html,
374
+ body {
375
+ touch-action: manipulation !important;
376
+ }
377
+
378
+ /* AppFrame: the drawer takes the sidebar column out of grid flow, so the
379
+ remaining in-flow items (center, details) land in tracks 1..2: give the
380
+ center every pixel and keep the details track at zero. The top padding
381
+ clears the status bar / notch for every in-flow surface (session header,
382
+ messages, composer); the absolutely-positioned drawer is unaffected (its
383
+ containing block is the frame's padding box, i.e. still the frame top). */
384
+ [data-mobile-nav="frame"] {
385
+ position: relative !important;
386
+ grid-template-columns: minmax(0, 1fr) 0 0 !important;
387
+ padding-top: env(safe-area-inset-top, 0px) !important;
388
+ }
389
+
390
+ /* The sidebar column (first grid child) becomes a left drawer. The drawer
391
+ hugs the sidebar content exactly (the wide sidebar carries an inline
392
+ width, ~280px): a fixed 92vw box would leave a white strip where the
393
+ container background shows beside the content.
394
+ Closed state: translateX(-110%) \u2014 more than -100% of the max-content
395
+ width \u2014 guarantees the whole drawer (and its shadow, had it one) leaves
396
+ the viewport. A mere -100% leaves a sliver on screen; -105% (as used
397
+ before) left 14px of the drawer plus a long 32px-blur shadow gradient
398
+ visible along the left edge of the main UI. No box-shadow at all: the
399
+ dimmed backdrop already separates drawer from content. */
400
+ [data-mobile-nav="frame"] > :first-child {
401
+ position: absolute !important;
402
+ inset: 0 auto 0 0 !important;
403
+ width: max-content !important;
404
+ max-width: 92vw !important;
405
+ z-index: 40 !important;
406
+ transform: translateX(-110%);
407
+ transition: transform .28s var(--ds-ease-in-out, ease-in-out);
408
+ background: var(--dsw-alias-bg-base, #ffffff);
409
+ /* Keep the drawer's own content below the status bar / notch: the drawer
410
+ spans the full frame height (its absolute containing block is the
411
+ frame's padding box, so the frame's own safe-area padding does NOT
412
+ reach it). The drawer background paints the status-bar strip, which
413
+ the client's theme-color meta matches, so the strip reads seamless. */
414
+ padding-top: env(safe-area-inset-top, 0px) !important;
415
+ /* Kill the official sidebarCol right border: with the backdrop the edge
416
+ reads cleanly, and the settings dialog (width:100% of this box) stays
417
+ pixel-flush with the drawer. */
418
+ border-right: none !important;
419
+ }
420
+
421
+ /* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
422
+ The open state must be transform:none \u2014 NOT translateX(0): an identity
423
+ transform still makes the drawer the containing block for fixed-position
424
+ descendants (the settings dialog's .VOzbGW_overlay is portaled into the
425
+ sidebar DOM). With the identity transform the wide settings sheet
426
+ (100vw-16) overflows the 280px drawer, the dialog's focus scrolls the
427
+ overflow:hidden drawer to scrollLeft=102, and every static child (plus the
428
+ fixed overlay) shifts 102px off-screen. With transform:none the overlay is
429
+ viewport-anchored: it dims the full screen and the sheet sits at left:8. */
430
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) > :first-child {
431
+ transform: none !important;
432
+ }
433
+
434
+ /* Drag handles are useless on touch and would float over the drawer. */
435
+ [data-side="sidebar"],
436
+ [data-side="details"] {
437
+ display: none !important;
438
+ }
439
+
440
+ /* --- Conversation text on mobile ---
441
+ The official message flow keeps desktop's 32px side gutters and 16px
442
+ type. On a phone: shrink the type a notch and widen the lines by
443
+ trimming the gutters (the sidebar drawer list keeps its size). The
444
+ flow's scroll container is the only _scroll element holding markdown
445
+ <p> paragraphs \u2014 the composer's own scroll (textarea) is excluded
446
+ via :has(p). */
447
+ /* The official main scroll body reserves scrollbar-gutter for desktop
448
+ scrollbars (8px), which shoves every column off-center on a phone.
449
+ Classic desktop scrollbars (Edge/Chrome) also occupy ~8-17px in a
450
+ phone-sized viewport, shifting the column further. Mobile scrolling
451
+ is touch/wheel, so remove the scrollbar entirely on phones: the
452
+ column is then exactly centered in every browser. */
453
+ [data-phase] [class$="_scrollBody"] {
454
+ scrollbar-gutter: auto !important;
455
+ scrollbar-width: none !important;
456
+ }
457
+ [data-phase] [class$="_scrollBody"]::-webkit-scrollbar {
458
+ display: none !important;
459
+ width: 0 !important;
460
+ height: 0 !important;
461
+ }
462
+ /* Message action rows (copy / run-time badges) can overflow the right
463
+ edge on narrow screens \u2014 keep them inside the message width. */
464
+ [data-phase] [class$="_actions"] {
465
+ overflow: hidden !important;
466
+ }
467
+ [data-phase] [class$="_actions"] [class$="_timeEnd"] {
468
+ flex: 0 1 auto !important;
469
+ min-width: 0 !important;
470
+ overflow: hidden !important;
471
+ text-overflow: ellipsis !important;
472
+ white-space: nowrap !important;
473
+ }
474
+
475
+ [data-phase] [class$="_scroll"]:has(p) {
476
+ padding-left: 20px !important;
477
+ padding-right: 20px !important;
478
+ font-size: 15px !important;
479
+ }
480
+ /* The official markdown styles set an explicit 16px on paragraphs and
481
+ list items, so the container's inherited 15px is not enough. User
482
+ messages render their text in a div whose class carries _text_
483
+ (16px too) \u2014 cover it as well. */
484
+ [data-phase] [class$="_scroll"]:has(p) p,
485
+ [data-phase] [class$="_scroll"]:has(p) li,
486
+ [data-phase] [class$="_scroll"]:has(p) [class*="_text_"] {
487
+ font-size: 15px !important;
488
+ }
489
+
490
+ /* --- Composer bottom row on mobile ---
491
+ The official row gives the model pill (trailing) flex:0 0 auto, which
492
+ squeezes the agent-permission pill (modes) down to 15px: the pill's
493
+ chevron then overflows on top of the model name. Let the permission
494
+ pill keep its natural width and let the model pill shrink instead.
495
+ Anchored by the composer card (:has(textarea)): row = last child,
496
+ tools = first child, permission pill = its 2nd child, model pill =
497
+ row's last child. */
498
+ [data-phase] [class*="_card"]:has(textarea) > :last-child {
499
+ gap: 8px !important;
500
+ }
501
+ [data-phase] [class*="_card"]:has(textarea) > :last-child > :first-child {
502
+ gap: 8px !important;
503
+ }
504
+ [data-phase] [class*="_card"]:has(textarea) > :last-child > :first-child > :nth-child(2) {
505
+ flex: 0 0 auto !important;
506
+ }
507
+ [data-phase] [class*="_card"]:has(textarea) > :last-child > :last-child {
508
+ flex: 1 1 auto !important;
509
+ min-width: 0 !important;
510
+ }
511
+
512
+ /* --- Session header on mobile ---
513
+ Layout goal: [toggle] [session title] [mode badge] in a row, with the
514
+ Session log capsule removed from the header (relocated to the drawer
515
+ footer). Stable structural hooks only:
516
+ [data-phase] header the session header element
517
+ header > :first-child titleRow (titleCluster + utilities)
518
+ header > :first-child > :last-child headerUtilities (Session log seat) */
519
+ [data-phase] header {
520
+ padding-right: 12px !important;
521
+ }
522
+ /* Give the title row a lane clear of the absolutely-placed toggle, then
523
+ balance the header: with header padding-right 12px, a 20px left
524
+ padding puts the title's geometric center exactly on the viewport
525
+ center (measured 195/195 at 390px). */
526
+ [data-phase] header > :first-child {
527
+ padding-left: 20px !important;
528
+ }
529
+ /* The directory toggle sits at the far left of the header (the header
530
+ is position:relative; the data-slot wrappers are display:contents). */
531
+ [data-mobile-nav="toggle"] {
532
+ position: absolute !important;
533
+ left: 8px !important;
534
+ top: 12px !important;
535
+ z-index: 2 !important;
536
+ }
537
+ /* The Files action sits at the FAR RIGHT of the header so it reads as a
538
+ distinct control from the directory toggle on the left (which opens
539
+ the history sidebar). */
540
+ [data-mobile-nav="files"] {
541
+ position: absolute !important;
542
+ left: auto !important;
543
+ right: 8px !important;
544
+ top: 12px !important;
545
+ z-index: 2 !important;
546
+ }
547
+ /* Session log download: gone from the header row on mobile (the utilities
548
+ seat holds only the session-log-export capsule). */
549
+ [data-phase] header > :first-child > :last-child {
550
+ display: none !important;
551
+ }
552
+
553
+ /* --- Settings dialog on mobile ---
554
+ Desktop: 800px two-column flex (188px nav + content). Mobile: a
555
+ near-full-width sheet \u2014 nav tabs wrap into rows on top, option rows
556
+ stay horizontal (title+description left, control right). Structural
557
+ selectors are scoped to the unique aria-modal dialog; every
558
+ settings-specific rule is gated with
559
+ :has(> :first-child > :last-child > button) \u2014 the settings nav tab
560
+ list holds <button> tabs, so the transient export dialog (the same
561
+ primitives Modal, header(title+close)+description+body) keeps its
562
+ official centered card layout. Requires :has() support
563
+ (Chromium 105+, 2022). */
564
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) {
565
+ position: absolute !important;
566
+ left: 8px !important;
567
+ /* Fixed top (no translateY): a transform on the panel combined with the
568
+ panel overflowing the max-content drawer shifts the fixed overlay's
569
+ coordinate frame, dragging the whole sidebar content off-screen. The
570
+ safe-area inset keeps the sheet below the status bar / notch. */
571
+ top: calc(env(safe-area-inset-top, 0px) + 12px) !important;
572
+ width: calc(100vw - 16px) !important;
573
+ max-width: calc(100vw - 16px) !important;
574
+ /* Height follows the content (no dead space under a short page); it
575
+ caps at 100dvh-24 (less the safe-area top) and the options area
576
+ scrolls only then. */
577
+ height: auto !important;
578
+ max-height: min(800px, calc(100vh - 24px - env(safe-area-inset-top, 0px))) !important;
579
+ max-height: min(800px, calc(100dvh - 24px - env(safe-area-inset-top, 0px))) !important;
580
+ flex-direction: column !important;
581
+ border-radius: 14px !important;
582
+ animation: dsh-mobile-nav-sheet-in .22s var(--ds-ease-out, ease-in-out);
583
+ }
584
+ /* The settings sheet's dimmed mask fades in with the panel (the mask is
585
+ the first child of the overlay that directly contains the sheet). */
586
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"]))) > :first-child {
587
+ animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);
588
+ }
589
+ @media (prefers-reduced-motion: reduce) {
590
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])),
591
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"]))) > :first-child {
592
+ animation: none !important;
593
+ }
594
+ }
595
+ /* The export dialog (not the settings sheet) must never overflow the
596
+ viewport: the official centered card can be wider than 390px. */
597
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)) {
598
+ max-width: calc(100vw - 32px) !important;
599
+ }
600
+ /* Nav bar: hide the "Settings" caption (redundant on a full-width sheet)
601
+ and wrap the tab list so every tab is visible \u2014 a horizontal scroll cut
602
+ the last tab ("Plugins") off with no affordance to scroll. */
603
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child {
604
+ width: 100% !important;
605
+ flex-direction: row !important;
606
+ align-items: center !important;
607
+ gap: 6px !important;
608
+ padding: 10px 12px 8px !important;
609
+ }
610
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :first-child {
611
+ display: none !important;
612
+ }
613
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :last-child {
614
+ flex-direction: row !important;
615
+ flex-wrap: wrap !important;
616
+ width: 100% !important;
617
+ gap: 6px !important;
618
+ overflow: visible !important;
619
+ }
620
+ /* Content toolbar (Open configuration file + close): spread to the edges
621
+ instead of clustering right with a dead zone on the left. The toolbar
622
+ children carry official auto-margins that would defeat space-between,
623
+ so neutralize them. The close button gets a round tappable base so it
624
+ reads as its own control, not part of the outline button. */
625
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child {
626
+ justify-content: space-between !important;
627
+ align-items: center !important;
628
+ padding: 0 12px !important;
629
+ min-height: 40px !important;
630
+ }
631
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child > * {
632
+ margin-left: 0 !important;
633
+ margin-right: 0 !important;
634
+ }
635
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child > :last-child {
636
+ width: 32px !important;
637
+ height: 32px !important;
638
+ border-radius: 50% !important;
639
+ display: inline-flex !important;
640
+ align-items: center !important;
641
+ justify-content: center !important;
642
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
643
+ }
644
+ /* Appearance mode cards: the official cube row renders three tall
645
+ vertical cards (~268px) that eat half the sheet. Turn them into a
646
+ compact horizontal trio (icon + label inline, equal widths).
647
+ Relies on the official cube-row class name of this version. */
648
+ [aria-modal="true"] [class$="_cubeRow"] {
649
+ gap: 6px !important;
650
+ }
651
+ [aria-modal="true"] [class$="_cubeRow"] > * {
652
+ flex: 1 1 0 !important;
653
+ flex-direction: row !important;
654
+ align-items: center !important;
655
+ justify-content: center !important;
656
+ gap: 6px !important;
657
+ padding: 10px 8px !important;
658
+ min-height: 0 !important;
659
+ }
660
+ /* Content: the options scroll area gets bottom breathing room so the last
661
+ row never sits flush against the sheet's rounded corner. */
662
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child {
663
+ flex: 1 1 auto !important;
664
+ min-height: 0 !important;
665
+ }
666
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :last-child {
667
+ padding: 0 12px 24px !important;
668
+ }
669
+
670
+ /* ---------- dsh-web-ui family compatibility ----------
671
+ The linxin666 plugin suite extends the shell frame directly:
672
+ - aionui-panel appends two trailing grid columns (explorer / preview)
673
+ plus absolute drag handles to [data-dsh-frame]; its 5-track inline
674
+ grid is already overridden above, but the handles and columns would
675
+ still float over the main UI. On mobile the columns leave the grid
676
+ as floating bottom sheets and keep their own visibility state \u2014
677
+ the suite's collapse chevron / preview tabs still work, so no
678
+ feature is lost. The task-board / ssh plugins inject sidebar
679
+ entries and center-column takeover panels; the entries need
680
+ spacing and the kanban needs scrollable columns. */
681
+
682
+ /* Touch devices: the drag handles are useless \u2014 the floating expand
683
+ button is the opener. */
684
+ .aionui-explorer-handle,
685
+ .aionui-preview-handle {
686
+ display: none !important;
687
+ }
688
+
689
+ /* Shared base: both columns leave the grid as floating panels. The
690
+ explorer is gated shut by default (its own persisted expanded state
691
+ must never cover the mobile UI on load); the header Files action opens
692
+ it via the frame marker below, and the sheet's own collapse chevron
693
+ clears it. Preview stays owned by the suite (hidden while no tab is
694
+ open). The per-column rules below override the geometry. */
695
+ [data-aionui-explorer-col],
696
+ [data-aionui-preview-col] {
697
+ position: fixed !important;
698
+ z-index: 55 !important;
699
+ background: var(--aion-bg-base, #ffffff) !important;
700
+ border-left: none !important;
701
+ }
702
+ /* Explorer (file tree) bottom sheet: bottom edge aligned exactly with
703
+ the composer card's bottom line \u2014 the card sits 36px above the
704
+ viewport bottom (8px composer padding + the 28px stats strip below
705
+ the card), so the sheet uses the same 36px bottom offset. */
706
+ [data-aionui-explorer-col] {
707
+ visibility: hidden !important;
708
+ left: 8px !important;
709
+ right: 8px !important;
710
+ top: auto !important;
711
+ bottom: 36px !important;
712
+ width: auto !important;
713
+ height: min(55dvh, 460px) !important;
714
+ max-height: calc(100dvh - 44px) !important;
715
+ border-radius: 14px !important;
716
+ overflow: hidden !important;
717
+ box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;
718
+ animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;
719
+ }
720
+ /* Preview (file content) bottom sheet. Gated shut by default: the suite
721
+ persists open preview tabs in localStorage and restores them on load,
722
+ which would pop the sheet over the fresh UI. The client only sets the
723
+ frame marker after the user taps a file row in the explorer; the
724
+ suite's own collapse chevron clears it via the visibility watcher. */
725
+ [data-aionui-preview-col] {
726
+ visibility: hidden !important;
727
+ position: fixed !important;
728
+ left: 8px !important;
729
+ right: 8px !important;
730
+ top: auto !important;
731
+ bottom: 40px !important;
732
+ width: auto !important;
733
+ height: min(50dvh, 420px) !important;
734
+ max-height: calc(100dvh - 48px) !important;
735
+ border-radius: 14px !important;
736
+ overflow: hidden !important;
737
+ box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;
738
+ z-index: 56 !important;
739
+ animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;
740
+ }
741
+ /* User-opened preview sheet (frame marker, set on file-row tap). */
742
+ [data-mobile-nav="frame"][data-aionui-preview-open] [data-aionui-preview-col] {
743
+ visibility: visible !important;
744
+ }
745
+ /* The Files action opens the explorer sheet (frame marker). */
746
+ [data-mobile-nav="frame"][data-aionui-explorer-open] [data-aionui-explorer-col] {
747
+ visibility: visible !important;
748
+ }
749
+ /* The open drawer must never sit under a sheet: while the frame is in the
750
+ narrow-expanded state both sheets yield (later in the file than the
751
+ open marker rule, so it wins at equal specificity). */
752
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) [data-aionui-explorer-col],
753
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) [data-aionui-preview-col] {
754
+ visibility: hidden !important;
755
+ }
756
+ /* The suite's own expand button reads the store state we bypass on
757
+ mobile \u2014 hide it; the header Files action is the opener. */
758
+ .aionui-floating-expand {
759
+ display: none !important;
760
+ }
761
+
762
+ /* dsh-web-ui sidebar entries (task board / ssh) sit flush against each
763
+ other \u2014 give the injected rows breathing room. */
764
+ button[data-dsh-taskboard-entry],
765
+ button[data-dsh-ssh-entry] {
766
+ margin-bottom: 8px !important;
767
+ }
768
+
769
+ /* Task board: five kanban columns at minmax(0,1fr) crush into ~78px phone
770
+ strips. Give every column a usable minimum and let the row scroll. */
771
+ [data-dsh-taskboard-board] > [class$="_columns"] {
772
+ grid-template-columns: repeat(5, minmax(240px, 1fr)) !important;
773
+ overflow-x: auto !important;
774
+ }
775
+ /* The floating button must not float over a takeover panel (task board /
776
+ ssh own the center column while active). */
777
+ html[data-dsh-taskboard-active] [data-mobile-nav="fab"],
778
+ html[data-dsh-ssh-active] [data-mobile-nav="fab"],
779
+ html[data-dsh-taskboard-active] [data-mobile-nav="backdrop"],
780
+ html[data-dsh-ssh-active] [data-mobile-nav="backdrop"] {
781
+ display: none !important;
782
+ }
783
+ /* Board header: let the search field take the slack instead of squeezing
784
+ the action buttons. */
785
+ [data-dsh-taskboard-board] > [class$="_boardHeader"] [class$="_search"] {
786
+ flex: 1 1 auto !important;
787
+ min-width: 80px !important;
788
+ }
789
+
790
+ /* ---------- dsh-web-ui polish: plugin market search ----------
791
+ The market tab row (Discover / Themes / Installed + the plugin search
792
+ box) is a no-wrap flex: at 390px the tabs plus the ~218px search box
793
+ (~475px total) overflow the ~334px sheet and the search box runs off
794
+ the right edge of the screen (it also forces a horizontal scrollbar on
795
+ the sheet's options area). Let the row wrap: the tabs keep the first
796
+ line and the search box gets its own full-width second line. */
797
+
798
+ [aria-modal="true"] [class$="_tabs"] {
799
+ flex-wrap: wrap !important;
800
+ row-gap: 8px !important;
801
+ }
802
+ [aria-modal="true"] [class$="_searchInline"] {
803
+ flex: 1 1 100% !important;
804
+ width: 100% !important;
805
+ max-width: 100% !important;
806
+ }
807
+
808
+ /* ---------- dsh-usage-stats polish: usage & balance panel ----------
809
+ The panel's stats row shows three token counters side by side
810
+ (today / month / total). The counters use tabular nowrap figures whose
811
+ min-content width overflows the ~336px panel body on a phone: figures
812
+ clip at the row's edges and the panel grows a horizontal scrollbar.
813
+ Stack the three counters vertically \u2014 full-width rows, so the figures
814
+ always fit. */
815
+
816
+ [class*="usg_"][class$="_statsRow"] {
817
+ flex-direction: column !important;
818
+ }
819
+ [class*="usg_"][class$="_stat"] {
820
+ flex: 0 0 auto !important;
821
+ width: 100% !important;
822
+ min-width: 0 !important;
823
+ }
824
+
825
+ /* ---------- dsh-web-ui polish: settings sheet ----------
826
+ The official dialog is a desktop two-column form; on a phone the
827
+ label/control split leaves a huge dead gap and long descriptions wrap
828
+ into tall stacks. Stack each row (text above, control full-width) and
829
+ compact the nav tabs into an even wrap. */
830
+
831
+ /* Nav tabs: a stable 3-per-row grid (two clean rows instead of a ragged
832
+ wrap) with tighter cells. */
833
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :last-child {
834
+ display: grid !important;
835
+ grid-template-columns: repeat(3, 1fr) !important;
836
+ gap: 6px !important;
837
+ }
838
+ [aria-modal="true"] [class$="_navCell"] {
839
+ padding: 6px 8px !important;
840
+ gap: 6px !important;
841
+ font-size: 13px !important;
842
+ justify-content: flex-start !important;
843
+ }
844
+ [aria-modal="true"] [class$="_navCell"] svg {
845
+ width: 14px !important;
846
+ height: 14px !important;
847
+ flex: none !important;
848
+ }
849
+ /* Setting rows: text on top, control below at full width. */
850
+ [aria-modal="true"] [class$="_section"] [class$="_row"] {
851
+ flex-direction: column !important;
852
+ align-items: stretch !important;
853
+ gap: 8px !important;
854
+ }
855
+ [aria-modal="true"] [class$="_section"] [class$="_row"] > :first-child {
856
+ width: 100% !important;
857
+ max-width: none !important;
858
+ }
859
+ [aria-modal="true"] [class$="_section"] [class$="_row"] > :last-child {
860
+ width: 100% !important;
861
+ max-width: none !important;
862
+ }
863
+ /* Appearance mode group: give the cube row a consistent bordered
864
+ segmented look (the official borders differ per state). */
865
+ [aria-modal="true"] [class$="_cubeRow"] > * {
866
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
867
+ }
868
+
869
+ /* ---------- dsh-web-ui polish: explorer sheet ----------
870
+ The aionui explorer was designed for a desktop side column: compact the
871
+ header, search box and tree rows so a phone shows more entries, and pad
872
+ the scroll bottom so the last row never sits flush on the edge. */
873
+
874
+ [data-aionui-explorer-col] [class$="_tabBar"] {
875
+ height: 36px !important;
876
+ }
877
+ [data-aionui-explorer-col] [class$="_tabBtn"],
878
+ [data-aionui-explorer-col] [class$="_tabBtnActive"] {
879
+ padding: 0 12px !important;
880
+ font-size: 13px !important;
881
+ }
882
+ [data-aionui-explorer-col] [class$="_searchBox"] {
883
+ height: 32px !important;
884
+ font-size: 13px !important;
885
+ }
886
+ [data-aionui-explorer-col] [class$="_treeRow"] {
887
+ height: 30px !important;
888
+ font-size: 13px !important;
889
+ }
890
+ [data-aionui-explorer-col] [class$="_treeRow"] svg {
891
+ width: 14px !important;
892
+ height: 14px !important;
893
+ }
894
+ [data-aionui-explorer-col] [class$="_scrollArea"] {
895
+ padding-bottom: 28px !important;
896
+ }
897
+
898
+ /* ---------- dsh-web-ui polish: drawer footer ----------
899
+ The injected footer actions (Files + Session log) become two equal pill
900
+ buttons instead of text-width capsules. */
901
+
902
+ /* The official footerActions row also hosts the remote-web-ui entry
903
+ row (two icon buttons); without wrapping the two groups squeeze each
904
+ other on one line. Wrap so each group gets its own full-width row. */
905
+ [data-mobile-nav="frame"] [class$="_footerActions"] {
906
+ flex-wrap: wrap !important;
907
+ gap: 6px !important;
908
+ }
909
+ [data-mobile-nav="drawer-actions"] {
910
+ width: 100% !important;
911
+ }
912
+ [data-mobile-nav="drawer-actions"] > button {
913
+ flex: 1 1 0 !important;
914
+ padding: 0 8px !important;
915
+ white-space: nowrap !important;
916
+ }
917
+
918
+ /* ---------- dsh-web-ui polish: floating pet ----------
919
+ The whale-girl pet (dsh-pet) floats at the viewport corner with a
920
+ persisted, draggable position. On phones the pet is scaled down so
921
+ it does not dominate the screen; the plugin's own drag + persist
922
+ still work (the position itself is left alone \u2014 the mobile default
923
+ position is seeded via the pet API to just above the composer). */
924
+
925
+ body > [class$="_float"]:has([class$="_sprite"][role="button"]) {
926
+ transform: scale(.66);
927
+ transform-origin: bottom right;
928
+ }
929
+ /* While a modal dialog (settings sheet / export) owns the screen the pet
930
+ floats ABOVE it and covers the dialog content; modal semantics say the
931
+ background is inert, so hide the pet for the modal's lifetime. */
932
+ body:has([aria-modal="true"]) > [class$="_float"]:has([class$="_sprite"][role="button"]) {
933
+ display: none !important;
934
+ }
935
+
936
+ /* ---------- dsh-web-ui polish: conversation stats line ----------
937
+ The official session-status row (turns / steps / LLM time / TTFT /
938
+ cache) is long. The client marks the exact row with
939
+ [data-mobile-nav="stats"] (text-anchored, hashed classes can't be
940
+ targeted). Layout: ONE fixed-height (28px) flex strip that scrolls
941
+ horizontally \u2014 the full metrics stream stays reachable by swiping,
942
+ the row never grows vertically, no ellipsis or fade, 12px gaps
943
+ between metric groups, a 2px scrollbar as the swipe affordance. */
944
+
945
+ [data-mobile-nav="stats"] {
946
+ display: flex !important;
947
+ flex-flow: row nowrap !important;
948
+ align-items: center !important;
949
+ width: 100% !important;
950
+ max-width: 100% !important;
951
+ min-width: 0 !important;
952
+ height: 28px !important;
953
+ min-height: 28px !important;
954
+ max-height: 28px !important;
955
+ box-sizing: border-box !important;
956
+ white-space: nowrap !important;
957
+ overflow-x: auto !important;
958
+ overflow-y: hidden !important;
959
+ -webkit-overflow-scrolling: touch;
960
+ overscroll-behavior-x: contain;
961
+ scrollbar-width: thin !important;
962
+ scrollbar-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .28)) transparent !important;
963
+ padding: 0 0 4px !important;
964
+ line-height: 20px !important;
965
+ font-size: 12px !important;
966
+ }
967
+ [data-mobile-nav="stats"]::-webkit-scrollbar {
968
+ height: 2px !important;
969
+ }
970
+ [data-mobile-nav="stats"]::-webkit-scrollbar-thumb {
971
+ background: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .3)) !important;
972
+ border-radius: 2px !important;
973
+ }
974
+ [data-mobile-nav="stats"]::-webkit-scrollbar-track {
975
+ background: transparent !important;
976
+ }
977
+ [data-mobile-nav="stats"] > * {
978
+ display: flex !important;
979
+ flex: 0 0 auto !important;
980
+ flex-flow: row nowrap !important;
981
+ align-items: center !important;
982
+ width: max-content !important;
983
+ min-width: max-content !important;
984
+ max-width: none !important;
985
+ white-space: nowrap !important;
986
+ margin-right: 12px !important;
987
+ padding: 0 !important;
988
+ }
989
+ [data-mobile-nav="stats"] > *:last-child {
990
+ margin-right: 0 !important;
991
+ }
992
+ [data-mobile-nav="stats"] * {
993
+ white-space: nowrap !important;
994
+ }
995
+
996
+ /* ---------- hero composer on mobile ----------
997
+ The official hero card carries a 2-line textarea plus a tall tool row,
998
+ which reads oversized on a phone. Tighten the empty-state rhythm: keep
999
+ the official centered hero, shrink the textarea line box, slim the card
1000
+ padding and the tool row, and close the gap under the headline. */
1001
+
1002
+ [data-phase="hero"] [class$="_card"]:has(textarea) {
1003
+ padding-top: 6px !important;
1004
+ gap: 8px !important;
1005
+ }
1006
+ /* The official composer autosizes the textarea and writes an inline
1007
+ height (2 lines on the hero empty state) on the textarea's scroll/grow
1008
+ wrappers. :placeholder-shown lets us collapse the EMPTY state to one
1009
+ line with !important; as soon as the user types, the pseudo-class no
1010
+ longer matches and the autosizer's inline height takes over again \u2014 so
1011
+ multi-line growth keeps working. */
1012
+ [data-phase="hero"] textarea:placeholder-shown {
1013
+ height: 28px !important;
1014
+ }
1015
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) > [class$="_scroll"],
1016
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) [class$="_grow"] {
1017
+ height: 28px !important;
1018
+ }
1019
+ [data-phase="hero"] [class$="_card"]:has(textarea) > [class$="_row"] {
1020
+ padding-top: 2px !important;
1021
+ }
1022
+ [data-phase="hero"] [class$="_headline"] {
1023
+ line-height: 1.15 !important;
1024
+ margin-bottom: 0 !important;
1025
+ }
1026
+ [data-phase="hero"] [class$="_stack"] {
1027
+ gap: 0 !important;
1028
+ }
1029
+ }
1030
+
1031
+ /* ---------- desktop: the mobile controls must never appear ---------- */
1032
+
1033
+ @media (min-width: 1024px) {
1034
+ [data-mobile-nav="toggle"],
1035
+ [data-mobile-nav="files"],
1036
+ [data-mobile-nav="fab"],
1037
+ [data-mobile-nav="backdrop"],
1038
+ [data-mobile-nav="session-log"],
1039
+ [data-mobile-nav="explorer"],
1040
+ [data-mobile-nav="drawer-actions"] {
1041
+ display: none !important;
1042
+ }
1043
+ }
1044
+ `;
1045
+
1046
+ // client/mobile/locales.ts
1047
+ var NS = "mobileNav";
1048
+ var zh = {
1049
+ "open": "\u6253\u5F00\u76EE\u5F55",
1050
+ "close": "\u6536\u8D77\u76EE\u5F55",
1051
+ "backdrop": "\u70B9\u51FB\u5173\u95ED\u76EE\u5F55",
1052
+ "sessionLog": "\u5BFC\u51FA\u4F1A\u8BDD\u65E5\u5FD7",
1053
+ "files": "\u6587\u4EF6\u6D4F\u89C8"
1054
+ };
1055
+ var en = {
1056
+ "open": "Open directory",
1057
+ "close": "Close directory",
1058
+ "backdrop": "Click to close directory",
1059
+ "sessionLog": "Session log",
1060
+ "files": "Files"
1061
+ };
1062
+
1063
+ // client/mobile/mobile-apply.tsx
1064
+ function mobileApply(ctx) {
1065
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-mobile-nav: dictionaries");
1066
+ ctx.effect(() => {
1067
+ const tag = document.createElement("style");
1068
+ tag.dataset.plugin = "@dsh-external/dsh-mobile-nav";
1069
+ tag.dataset.pluginCss = "@dsh-external/dsh-mobile-nav/mobile.css";
1070
+ tag.textContent = MOBILE_CSS;
1071
+ document.head.appendChild(tag);
1072
+ return () => {
1073
+ tag.remove();
1074
+ };
1075
+ }, "dsh-mobile-nav: styles");
1076
+ ctx.effect(() => {
1077
+ const narrow = window.matchMedia("(max-width: 1023px)");
1078
+ const viewport = document.querySelector('meta[name="viewport"]');
1079
+ const originalViewport = viewport?.content ?? "";
1080
+ const themeMeta = document.createElement("meta");
1081
+ themeMeta.name = "theme-color";
1082
+ const bodyBg = () => getComputedStyle(document.body).backgroundColor;
1083
+ const sync = () => {
1084
+ if (viewport !== null) viewport.content = "width=device-width, initial-scale=1, viewport-fit=cover";
1085
+ themeMeta.content = bodyBg();
1086
+ if (themeMeta.parentElement === null) document.head.appendChild(themeMeta);
1087
+ };
1088
+ const restore = () => {
1089
+ if (viewport !== null) viewport.content = originalViewport;
1090
+ themeMeta.remove();
1091
+ };
1092
+ const onGestureStart = (event) => event.preventDefault();
1093
+ if (narrow.matches) sync();
1094
+ const onChange = (event) => event.matches ? sync() : restore();
1095
+ narrow.addEventListener("change", onChange);
1096
+ const observer = new MutationObserver(() => {
1097
+ if (narrow.matches) themeMeta.content = bodyBg();
1098
+ });
1099
+ observer.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
1100
+ document.addEventListener("gesturestart", onGestureStart);
1101
+ return () => {
1102
+ narrow.removeEventListener("change", onChange);
1103
+ observer.disconnect();
1104
+ document.removeEventListener("gesturestart", onGestureStart);
1105
+ restore();
1106
+ };
1107
+ }, "dsh-mobile-nav: status bar theme + viewport + zoom guard");
1108
+ ctx.effect(() => {
1109
+ const narrow = window.matchMedia("(max-width: 1023px)");
1110
+ if (!narrow.matches) return () => {
1111
+ };
1112
+ const onChevronClick = (event) => {
1113
+ const target = event.target;
1114
+ if (target === null || !target.closest(".aionui-collapse-chevron")) return;
1115
+ document.querySelector('[data-mobile-nav="frame"]')?.removeAttribute("data-aionui-explorer-open");
1116
+ };
1117
+ document.addEventListener("click", onChevronClick, true);
1118
+ return () => document.removeEventListener("click", onChevronClick, true);
1119
+ }, "dsh-mobile-nav: aionui explorer close marker");
1120
+ ctx.effect(() => {
1121
+ const narrow = window.matchMedia("(max-width: 1023px)");
1122
+ if (!narrow.matches) return () => {
1123
+ };
1124
+ const frame = () => document.querySelector('[data-mobile-nav="frame"]');
1125
+ const onTap = (event) => {
1126
+ const target = event.target;
1127
+ if (target === null) return;
1128
+ if (target.closest('[data-aionui-explorer-col] [class$="_treeRow"]') === null) return;
1129
+ frame()?.setAttribute("data-aionui-preview-open", "");
1130
+ };
1131
+ const sync = () => {
1132
+ const pv = document.querySelector("[data-aionui-preview-col]");
1133
+ if (pv === null) return;
1134
+ if (getComputedStyle(pv).visibility === "hidden") frame()?.removeAttribute("data-aionui-preview-open");
1135
+ };
1136
+ document.addEventListener("click", onTap, true);
1137
+ const observer = new MutationObserver(sync);
1138
+ observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ["style"] });
1139
+ sync();
1140
+ return () => {
1141
+ document.removeEventListener("click", onTap, true);
1142
+ observer.disconnect();
1143
+ };
1144
+ }, "dsh-mobile-nav: preview sheet open marker");
1145
+ ctx.effect(() => {
1146
+ const narrow = window.matchMedia("(max-width: 1023px)");
1147
+ if (!narrow.matches) return () => {
1148
+ };
1149
+ const moveTps = (stats) => {
1150
+ if ([...stats.children].some((c) => /^TPS\s+\d/.test((c.textContent ?? "").trim()))) return;
1151
+ const stack = stats.closest('[class$="_composerStack"]');
1152
+ if (stack === null) return;
1153
+ for (const el of stack.querySelectorAll("div")) {
1154
+ const text = (el.textContent ?? "").trim();
1155
+ if (!/^TPS\s+\d/.test(text)) continue;
1156
+ if (el.children.length > 0) continue;
1157
+ stats.appendChild(el);
1158
+ return;
1159
+ }
1160
+ };
1161
+ const mark = () => {
1162
+ for (const root of document.querySelectorAll('[data-phase] [class$="_root"]')) {
1163
+ if (root.closest('[class$="_composerStack"]') === null) continue;
1164
+ const text = root.textContent ?? "";
1165
+ if (!/(turns|steps|\bLLM\b|轮|步)/.test(text)) continue;
1166
+ if (root.querySelector("textarea") !== null) continue;
1167
+ root.setAttribute("data-mobile-nav", "stats");
1168
+ moveTps(root);
1169
+ return;
1170
+ }
1171
+ };
1172
+ const observer = new MutationObserver(mark);
1173
+ observer.observe(document.body, { childList: true, subtree: true });
1174
+ mark();
1175
+ return () => {
1176
+ observer.disconnect();
1177
+ };
1178
+ }, "dsh-mobile-nav: stats line marker");
1179
+ ctx.effect(() => {
1180
+ const narrow = window.matchMedia("(max-width: 1023px)");
1181
+ if (!narrow.matches) return () => {
1182
+ };
1183
+ const cols = ["[data-aionui-explorer-col]", "[data-aionui-preview-col]"];
1184
+ const seen = /* @__PURE__ */ new Map();
1185
+ const play = (el) => {
1186
+ el.animate(
1187
+ [
1188
+ { opacity: 0, transform: "translateY(28px)" },
1189
+ { opacity: 1, transform: "none" }
1190
+ ],
1191
+ { duration: 280, easing: "cubic-bezier(.16, 1, .3, 1)", fill: "backwards" }
1192
+ );
1193
+ };
1194
+ const check = () => {
1195
+ for (const sel of cols) {
1196
+ const el = document.querySelector(sel);
1197
+ if (el === null) continue;
1198
+ const visible = getComputedStyle(el).visibility === "visible";
1199
+ const prev = seen.get(sel) ?? false;
1200
+ if (visible && !prev) play(el);
1201
+ seen.set(sel, visible);
1202
+ }
1203
+ };
1204
+ const observer = new MutationObserver(check);
1205
+ observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ["style", "class", "data-aionui-explorer-open"] });
1206
+ check();
1207
+ return () => {
1208
+ observer.disconnect();
1209
+ };
1210
+ }, "dsh-mobile-nav: sheet rise animation replay");
1211
+ ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1212
+ name: "conversation.session.header.actions",
1213
+ id: "mobile-nav-toggle",
1214
+ order: 10,
1215
+ locale: NS,
1216
+ inject: () => ({
1217
+ toggleSidebar: () => ctx.layout.toggleSidebar()
1218
+ })
1219
+ }, MobileNavToggle));
1220
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1221
+ name: "shell.overlay",
1222
+ id: "mobile-nav-overlay",
1223
+ order: 10,
1224
+ locale: NS,
1225
+ inject: () => ({
1226
+ toggleSidebar: () => ctx.layout.toggleSidebar()
1227
+ })
1228
+ }, MobileNavOverlay));
1229
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1230
+ name: "sidebar.footer.action",
1231
+ id: "mobile-nav-session-log",
1232
+ order: 10,
1233
+ locale: NS,
1234
+ inject: () => ({
1235
+ downloadSessionLog: (sessionId) => ctx.sessionLogDownload.download(sessionId),
1236
+ toggleSidebar: () => ctx.layout.toggleSidebar()
1237
+ })
1238
+ }, MobileDrawerFooter));
1239
+ }
1240
+
55
1241
  // client/index.jsx
56
1242
  var name = "dsh-pocket";
57
- var inject = ["slots", "connection"];
1243
+ var inject = ["slots", "connection", "layout", "locale", "sessionLogDownload"];
58
1244
  var styles = {
59
1245
  card: { background: "var(--dsw-alias-bg-layer-1,#fff)", border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", borderRadius: 12, padding: "14px 16px", maxWidth: 480 },
60
1246
  block: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", marginTop: 12, paddingTop: 12 },
@@ -66,9 +1252,9 @@ var styles = {
66
1252
  warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12 }
67
1253
  };
68
1254
  function PocketSettingsTab({ rpcCall }) {
69
- const [status, setStatus] = (0, import_react.useState)(null);
70
- const [busy, setBusy] = (0, import_react.useState)(false);
71
- const [error, setError] = (0, import_react.useState)(null);
1255
+ const [status, setStatus] = (0, import_react2.useState)(null);
1256
+ const [busy, setBusy] = (0, import_react2.useState)(false);
1257
+ const [error, setError] = (0, import_react2.useState)(null);
72
1258
  const call = async (endpoint, payload) => {
73
1259
  const res = await rpcCall(endpoint, payload);
74
1260
  if (!res?.ok) throw new Error(res?.error?.message ?? "RPC failed");
@@ -80,7 +1266,7 @@ function PocketSettingsTab({ rpcCall }) {
80
1266
  } catch {
81
1267
  }
82
1268
  };
83
- (0, import_react.useEffect)(() => {
1269
+ (0, import_react2.useEffect)(() => {
84
1270
  load();
85
1271
  const t = setInterval(load, 3e3);
86
1272
  return () => clearInterval(t);
@@ -104,51 +1290,52 @@ function PocketSettingsTab({ rpcCall }) {
104
1290
  };
105
1291
  const lanUrl = status?.lanUrl;
106
1292
  const tunnelUrl = status?.tunnelUrl;
107
- return (0, import_react.createElement)(
1293
+ return (0, import_react2.createElement)(
108
1294
  "div",
109
1295
  { style: styles.card },
110
- (0, import_react.createElement)(
1296
+ (0, import_react2.createElement)(
111
1297
  "div",
112
1298
  null,
113
- (0, import_react.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
114
- (0, import_react.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
1299
+ (0, import_react2.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
1300
+ (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
115
1301
  ),
116
1302
  // 局域网
117
- (0, import_react.createElement)(
1303
+ (0, import_react2.createElement)(
118
1304
  "div",
119
1305
  { style: styles.block },
120
- (0, import_react.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09| LAN"),
121
- lanUrl ? (0, import_react.createElement)(
1306
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09| LAN"),
1307
+ lanUrl ? (0, import_react2.createElement)(
122
1308
  "div",
123
1309
  null,
124
- (0, import_react.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
125
- (0, import_react.createElement)("div", { style: styles.code }, lanUrl),
126
- (0, import_react.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00")
127
- ) : (0, import_react.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
1310
+ (0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
1311
+ (0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
1312
+ (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00")
1313
+ ) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
128
1314
  ),
129
1315
  // 公网
130
- (0, import_react.createElement)(
1316
+ (0, import_react2.createElement)(
131
1317
  "div",
132
1318
  { style: styles.block },
133
- (0, import_react.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09| Anywhere"),
134
- tunnelUrl ? (0, import_react.createElement)(
1319
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09| Anywhere"),
1320
+ tunnelUrl ? (0, import_react2.createElement)(
135
1321
  "div",
136
1322
  null,
137
- (0, import_react.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
138
- (0, import_react.createElement)("div", { style: styles.code }, tunnelUrl),
139
- (0, import_react.createElement)("div", { style: styles.muted }, "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u4F1A\u53D8\uFF09"),
140
- (0, import_react.createElement)("button", { style: styles.btn, onClick: stopTunnel }, "\u5173\u95ED\u516C\u7F51 | Stop")
141
- ) : (0, import_react.createElement)(
1323
+ (0, import_react2.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
1324
+ (0, import_react2.createElement)("div", { style: styles.code }, tunnelUrl),
1325
+ (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u4F1A\u53D8\uFF09"),
1326
+ (0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, "\u5173\u95ED\u516C\u7F51 | Stop")
1327
+ ) : (0, import_react2.createElement)(
142
1328
  "div",
143
1329
  null,
144
- (0, import_react.createElement)("button", { style: styles.primary, onClick: startTunnel, disabled: busy }, busy ? "\u5F00\u542F\u4E2D\u2026\uFF08\u9996\u6B21\u9700\u4E0B\u8F7D cloudflared\uFF09" : "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE | Enable anywhere"),
145
- (0, import_react.createElement)("div", { style: styles.warn, marginTop: 8 }, "\u26A0\uFE0F DSH \u80FD\u6267\u884C\u7535\u8111\u4EE3\u7801\uFF1A\u4E8C\u7EF4\u7801/URL \u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u52FF\u53D1\u7ED9\u522B\u4EBA")
1330
+ (0, import_react2.createElement)("button", { style: styles.primary, onClick: startTunnel, disabled: busy }, busy ? "\u5F00\u542F\u4E2D\u2026\uFF08\u9996\u6B21\u9700\u4E0B\u8F7D cloudflared\uFF09" : "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE | Enable anywhere"),
1331
+ (0, import_react2.createElement)("div", { style: styles.warn, marginTop: 8 }, "\u26A0\uFE0F DSH \u80FD\u6267\u884C\u7535\u8111\u4EE3\u7801\uFF1A\u4E8C\u7EF4\u7801/URL \u5C31\u662F\u94A5\u5319\uFF0C\u8BF7\u52FF\u53D1\u7ED9\u522B\u4EBA")
146
1332
  )
147
1333
  ),
148
- error ? (0, import_react.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null
1334
+ error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null
149
1335
  );
150
1336
  }
151
1337
  function apply(ctx) {
1338
+ mobileApply(ctx);
152
1339
  const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(POCKET_RPC_CHANNEL, endpoint, payload, signal);
153
1340
  ctx.slots.inject(
154
1341
  "settings.plugins.tab",