dsh-zen-remote 1.1.11 → 1.1.13

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 (85) hide show
  1. package/AGENTS.md +24 -0
  2. package/README.md +7 -3
  3. package/README.zh-CN.md +7 -3
  4. package/docs/interface.md +5 -2
  5. package/dsh-push.mjs +25 -1
  6. package/lan-gate.mjs +16 -1
  7. package/lib/client.js +684 -43
  8. package/lib/lan-gate-server.cjs +107 -4
  9. package/lib/types/client/MobileHome.d.ts +7 -2
  10. package/lib/types/client/MobileHome.d.ts.map +1 -1
  11. package/lib/types/client/MobileSessionInfo.d.ts +2 -2
  12. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  13. package/lib/types/client/compat/store.d.ts +62 -0
  14. package/lib/types/client/compat/store.d.ts.map +1 -0
  15. package/lib/types/client/compat/types.d.ts +84 -0
  16. package/lib/types/client/compat/types.d.ts.map +1 -0
  17. package/lib/types/client/debug.d.ts +1 -1
  18. package/lib/types/client/debug.d.ts.map +1 -1
  19. package/lib/types/client/effects/aionui-compat.d.ts +1 -1
  20. package/lib/types/client/effects/aionui-compat.d.ts.map +1 -1
  21. package/lib/types/client/effects/gestures.d.ts +1 -1
  22. package/lib/types/client/effects/gestures.d.ts.map +1 -1
  23. package/lib/types/client/effects/header-status.d.ts +1 -1
  24. package/lib/types/client/effects/header-status.d.ts.map +1 -1
  25. package/lib/types/client/effects/keyboard-avoid.d.ts +1 -1
  26. package/lib/types/client/effects/keyboard-avoid.d.ts.map +1 -1
  27. package/lib/types/client/effects/keyboard-guard.d.ts +1 -1
  28. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
  29. package/lib/types/client/effects/modal-back.d.ts +1 -1
  30. package/lib/types/client/effects/modal-back.d.ts.map +1 -1
  31. package/lib/types/client/effects/model-sheet-extras.d.ts +33 -0
  32. package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -0
  33. package/lib/types/client/effects/native-trigger-overlay.d.ts +1 -1
  34. package/lib/types/client/effects/native-trigger-overlay.d.ts.map +1 -1
  35. package/lib/types/client/effects/phone-chrome.d.ts +1 -1
  36. package/lib/types/client/effects/phone-chrome.d.ts.map +1 -1
  37. package/lib/types/client/effects/turn-fold.d.ts +1 -1
  38. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  39. package/lib/types/client/effects/welcome-notice.d.ts +1 -1
  40. package/lib/types/client/effects/welcome-notice.d.ts.map +1 -1
  41. package/lib/types/client/effects/workbench-ref-close.d.ts +1 -1
  42. package/lib/types/client/effects/workbench-ref-close.d.ts.map +1 -1
  43. package/lib/types/client/index.d.ts +1 -1
  44. package/lib/types/client/index.d.ts.map +1 -1
  45. package/lib/types/client/nav-store.d.ts +2 -2
  46. package/lib/types/client/nav-store.d.ts.map +1 -1
  47. package/lib/types/client/session-dot.d.ts +11 -3
  48. package/lib/types/client/session-dot.d.ts.map +1 -1
  49. package/lib/types/client/styles/compat.css.d.ts +1 -1
  50. package/lib/types/client/styles/compat.css.d.ts.map +1 -1
  51. package/lib/types/client/styles/composer.css.d.ts +1 -1
  52. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  53. package/lib/types/client/styles/info.css.d.ts +1 -1
  54. package/lib/types/client/styles/info.css.d.ts.map +1 -1
  55. package/package.json +49 -26
  56. package/pwa/app.css +47 -7
  57. package/scripts/check-client-externals.mjs +48 -0
  58. package/scripts/check-compat-store.mjs +113 -0
  59. package/scripts/check-host-shadow.mjs +145 -0
  60. package/scripts/check-info-sheet-stacking.mjs +55 -0
  61. package/src/client/MobileAttachButton.tsx +1 -1
  62. package/src/client/MobileHome.tsx +33 -7
  63. package/src/client/MobileSessionInfo.tsx +6 -4
  64. package/src/client/chips-store.ts +1 -1
  65. package/src/client/compat/store.ts +205 -0
  66. package/src/client/compat/types.ts +115 -0
  67. package/src/client/debug.ts +1 -1
  68. package/src/client/effects/aionui-compat.ts +1 -1
  69. package/src/client/effects/gestures.ts +1 -1
  70. package/src/client/effects/header-status.ts +22 -3
  71. package/src/client/effects/keyboard-avoid.ts +1 -1
  72. package/src/client/effects/keyboard-guard.ts +1 -1
  73. package/src/client/effects/modal-back.ts +1 -1
  74. package/src/client/effects/model-sheet-extras.ts +159 -0
  75. package/src/client/effects/native-trigger-overlay.ts +1 -1
  76. package/src/client/effects/phone-chrome.ts +1 -1
  77. package/src/client/effects/turn-fold.ts +1 -1
  78. package/src/client/effects/welcome-notice.ts +1 -1
  79. package/src/client/effects/workbench-ref-close.ts +1 -1
  80. package/src/client/index.tsx +70 -6
  81. package/src/client/nav-store.ts +2 -2
  82. package/src/client/session-dot.ts +13 -4
  83. package/src/client/styles/compat.css.ts +43 -11
  84. package/src/client/styles/composer.css.ts +184 -9
  85. package/src/client/styles/info.css.ts +21 -0
package/lib/client.js CHANGED
@@ -316,12 +316,188 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
316
316
  }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconDownloadOutline16, { size: 14 }), (0, jsx_runtime_1.jsx)("span", { children: t('sessionLog') })] })] }));
317
317
  }
318
318
  };
319
+ __modules["compat/types.js"] = function (require, module, exports) {
320
+ "use strict";
321
+ Object.defineProperty(exports, "__esModule", { value: true });
322
+ exports.agentPresetOf = agentPresetOf;
323
+ exports.hasPendingInteraction = hasPendingInteraction;
324
+ /**
325
+ * 0.1.1's session row carries `agentPreset`; 0.1.2 dropped it. Reading it
326
+ * must not depend on which version's types are in scope in the current
327
+ * file, so narrow through unknown.
328
+ * @param row - a session row of either version.
329
+ * @returns the agent preset name, or undefined when absent.
330
+ */
331
+ function agentPresetOf(row) {
332
+ const agentPreset = row?.agentPreset;
333
+ return typeof agentPreset === 'string' ? agentPreset : undefined;
334
+ }
335
+ /**
336
+ * 0.1.1's session row carries `pendingInteraction` (the pending-interaction
337
+ * state behind the sidebar's yellow dot); 0.1.2 serves it from the
338
+ * uiSession service instead. This answers only "does this row carry a
339
+ * pending interaction".
340
+ * @param row - a session row of either version.
341
+ * @returns true when the row has a pending interaction.
342
+ */
343
+ function hasPendingInteraction(row) {
344
+ const pending = row?.pendingInteraction;
345
+ return pending !== undefined && pending !== null;
346
+ }
347
+ };
348
+ __modules["compat/store.js"] = function (require, module, exports) {
349
+ "use strict";
350
+ Object.defineProperty(exports, "__esModule", { value: true });
351
+ exports.createSnapshotStore = createSnapshotStore;
352
+ exports.defineStore = defineStore;
353
+ exports.workspaceTitleOf = workspaceTitleOf;
354
+ /**
355
+ * Create a snapshot store with optional whole-value localStorage persistence.
356
+ * The 0.1.1 `opts.flush: 'raf' | 'sync'` mode is deliberately not
357
+ * implemented: this plugin never used it (chips-store.ts persists
358
+ * synchronously and React reads via useSyncExternalStore).
359
+ * @param init - initial state.
360
+ * @param opts - optional persistence key.
361
+ * @returns the store.
362
+ */
363
+ function createSnapshotStore(init, opts) {
364
+ let state = init;
365
+ const listeners = new Set();
366
+ const persistName = opts?.persist?.name;
367
+ // Rehydrate once at construction: a persisted whole value replaces the
368
+ // initial state wholesale. Failures (quota, private mode, corrupted JSON)
369
+ // only log — a broken persisted value must not take the store down, and a
370
+ // store that cannot persist must still work in memory.
371
+ if (persistName !== undefined && typeof localStorage !== 'undefined') {
372
+ try {
373
+ const raw = localStorage.getItem(persistName);
374
+ if (raw !== null)
375
+ state = JSON.parse(raw);
376
+ }
377
+ catch (error) {
378
+ console.error(`snapshot store '${persistName}' rehydration failed:`, error);
379
+ }
380
+ }
381
+ const persist = () => {
382
+ if (persistName === undefined || typeof localStorage === 'undefined')
383
+ return;
384
+ try {
385
+ localStorage.setItem(persistName, JSON.stringify(state));
386
+ }
387
+ catch (error) {
388
+ console.error(`snapshot store '${persistName}' persistence failed:`, error);
389
+ }
390
+ };
391
+ const notify = () => {
392
+ // Iterate a snapshot of the set so a callback that unsubscribes cannot
393
+ // make the loop skip listeners registered after it.
394
+ for (const fn of [...listeners])
395
+ fn();
396
+ };
397
+ return {
398
+ getSnapshot: () => state,
399
+ subscribe: (fn) => {
400
+ listeners.add(fn);
401
+ return () => {
402
+ listeners.delete(fn);
403
+ };
404
+ },
405
+ update: (mutator) => {
406
+ // ponytail: official uses immer's structural sharing; here we copy one
407
+ // level shallowly (array spread / object spread) and let the mutator
408
+ // edit that copy before swapping it in wholesale. That only supports
409
+ // one-level-deep draft writes — fine while both plugin stores stay
410
+ // flat ({view, workspace}, Record<string, boolean>); if a state ever
411
+ // nests, switch this to immer instead of deepening the copy.
412
+ const draft = (Array.isArray(state) ? [...state] : { ...state });
413
+ mutator(draft);
414
+ state = draft;
415
+ notify();
416
+ persist();
417
+ },
418
+ set: (next) => {
419
+ state = next;
420
+ notify();
421
+ persist();
422
+ },
423
+ };
424
+ }
425
+ /**
426
+ * Declare a store: an init lambda (fresh state per instance), an optional
427
+ * persistence key, and the full write set as pure draft mutators. Returns
428
+ * the registration currency of the slot system's store seat.
429
+ *
430
+ * The `A & ActionsDecl<T>` actions position is load-bearing: T resolves from
431
+ * `init` in the first inference round, and the intersection then
432
+ * contextually types each mutator's draft parameter, so call sites write
433
+ * `(draft, view: MobileView) => { … }` without annotating the draft. Keep
434
+ * the intersection — dropping it makes nav-store.ts fail to typecheck.
435
+ *
436
+ * Persist-key rules mirror the official defineStore: `decl.persist` alone at
437
+ * root scope, suffixed with `.{scopeKey}` per session scope, none when
438
+ * `decl.persist` is absent.
439
+ * @param decl - init/persist/actions declaration.
440
+ * @returns the store handle.
441
+ */
442
+ function defineStore(decl) {
443
+ return {
444
+ spec: decl,
445
+ create(scopeKey) {
446
+ const persistKey = decl.persist === undefined
447
+ ? undefined
448
+ : scopeKey === undefined
449
+ ? decl.persist
450
+ : `${decl.persist}.${scopeKey}`;
451
+ const store = createSnapshotStore(decl.init(), persistKey !== undefined ? { persist: { name: persistKey } } : undefined);
452
+ // TS cannot verify an object that is filled key-by-key from
453
+ // Object.keys(decl.actions) satisfies the BakedActions mapped type, so
454
+ // the map is built loosely and narrowed once at the contract boundary;
455
+ // at runtime every declared action key is present (the declaration's
456
+ // own keys are the ones iterated).
457
+ const actions = {};
458
+ for (const [key, mutate] of Object.entries(decl.actions)) {
459
+ actions[key] = (...params) => {
460
+ store.update((draft) => {
461
+ mutate(draft, ...params);
462
+ });
463
+ };
464
+ }
465
+ return {
466
+ actions: actions,
467
+ getSnapshot: () => store.getSnapshot(),
468
+ subscribe: (fn) => store.subscribe(fn),
469
+ store,
470
+ clearPersisted: () => {
471
+ if (persistKey === undefined || typeof localStorage === 'undefined')
472
+ return;
473
+ try {
474
+ localStorage.removeItem(persistKey);
475
+ }
476
+ catch {
477
+ // Storage gone or denied: nothing to clean up anyway.
478
+ }
479
+ },
480
+ };
481
+ },
482
+ };
483
+ }
484
+ /**
485
+ * Last path segment of a workspace directory, for showing "which workspace
486
+ * is this session in" on the mobile info card. 0.1.1's one-line
487
+ * implementation, kept verbatim.
488
+ * @param cwd - a workspace path (posix or windows separators).
489
+ * @returns the trailing name, or '' when no non-empty segment exists.
490
+ */
491
+ function workspaceTitleOf(cwd) {
492
+ return cwd.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? '';
493
+ }
494
+ };
319
495
  __modules["nav-store.js"] = function (require, module, exports) {
320
496
  "use strict";
321
497
  Object.defineProperty(exports, "__esModule", { value: true });
322
498
  exports.SESSION_INFO_EVENT = exports.GO_HOME_EVENT = void 0;
323
499
  exports.createNavStore = createNavStore;
324
- const client_1 = require("@deepseek-ai/dsh-client-runtime/client");
500
+ const store_ts_1 = require("./compat/store.js");
325
501
  /**
326
502
  * `window` event a `conversation.session.header.*` slot (session scope)
327
503
  * fires to move the ROOT-scope nav store back to `home` — see
@@ -362,7 +538,7 @@ exports.SESSION_INFO_EVENT = 'dsh-mobile-nav:session-info';
362
538
  * one apply().
363
539
  */
364
540
  function createNavStore() {
365
- return (0, client_1.defineStore)({
541
+ return (0, store_ts_1.defineStore)({
366
542
  init: () => ({ view: 'home', workspace: null }),
367
543
  actions: {
368
544
  /**
@@ -505,16 +681,25 @@ __modules["session-dot.js"] = function (require, module, exports) {
505
681
  "use strict";
506
682
  Object.defineProperty(exports, "__esModule", { value: true });
507
683
  exports.dotState = dotState;
684
+ const types_ts_1 = require("./compat/types.js");
508
685
  /**
509
686
  * Status dot state of one session, matching the official sidebar semantics.
510
687
  * Shared by the home-screen row dots (MobileHome.tsx) and the session
511
688
  * header's running indicator (effects/header-status.ts), which reads it
512
689
  * outside React — see that module for why.
513
- * @param row - session summary.
690
+ *
691
+ * Pending interaction has two version-specific sources: `pending` is the
692
+ * 0.1.2 source (the uiSession service / useSessionPendingInteraction map,
693
+ * which this dot's caller resolves), and `hasPendingInteraction(row)` is the
694
+ * 0.1.1 source (the row's own field). Each version hits exactly one of the
695
+ * two: on 0.1.2 the row field is gone so the row check is always false, and
696
+ * on 0.1.1 `pending` is always false because the caller has no uiSession.
697
+ * @param row - session row.
698
+ * @param pending - whether the uiSession pending map has this row (0.1.2).
514
699
  * @returns the dot state, or undefined when the row needs no dot.
515
700
  */
516
- function dotState(row) {
517
- if (row.pendingInteraction !== undefined)
701
+ function dotState(row, pending) {
702
+ if (pending === true || (0, types_ts_1.hasPendingInteraction)(row))
518
703
  return 'warning';
519
704
  if (row.running)
520
705
  return 'ongoing';
@@ -529,7 +714,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
529
714
  exports.isChipEnabled = isChipEnabled;
530
715
  exports.toggleChip = toggleChip;
531
716
  exports.useChipsPrefs = useChipsPrefs;
532
- const client_1 = require("@deepseek-ai/dsh-client-runtime/client");
717
+ const store_ts_1 = require("./compat/store.js");
533
718
  const react_1 = require("react");
534
719
  /**
535
720
  * Plain `createSnapshotStore` (runtime contract/store.d.ts), not a
@@ -544,7 +729,7 @@ const react_1 = require("react");
544
729
  * localStorage-backed identity, so re-importing the module always reaches
545
730
  * the same store.
546
731
  */
547
- const store = (0, client_1.createSnapshotStore)({}, { persist: { name: 'dsh-mobile-nav.chips' } });
732
+ const store = (0, store_ts_1.createSnapshotStore)({}, { persist: { name: 'dsh-mobile-nav.chips' } });
548
733
  /** @param prefs - current snapshot. @param id - chip id. @returns whether the chip should render (default true). */
549
734
  function isChipEnabled(prefs, id) {
550
735
  return prefs[id] !== false;
@@ -877,6 +1062,7 @@ exports.MobileHome = MobileHome;
877
1062
  const jsx_runtime_1 = require("react/jsx-runtime");
878
1063
  const react_1 = require("react");
879
1064
  const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
1065
+ const types_ts_1 = require("./compat/types.js");
880
1066
  const nav_store_ts_1 = require("./nav-store.js");
881
1067
  const history_nav_ts_1 = require("./history-nav.js");
882
1068
  /** The phone shell's one page-stack step: session list -> session. */
@@ -885,6 +1071,16 @@ const session_dot_ts_1 = require("./session-dot.js");
885
1071
  const MobileHomeChips_tsx_1 = require("./MobileHomeChips.js");
886
1072
  /** Phone breakpoint: below the tablet range, where the app-shell layout applies. */
887
1073
  const PHONE_QUERY = '(max-width: 767px)';
1074
+ /**
1075
+ * Empty pending-interaction map singleton: when the 0.1.2 source is absent
1076
+ * the fallback hook returns THIS map, so the selector sees one stable
1077
+ * reference across renders — a fresh Map per render would make every
1078
+ * selector result a new reference and loop the list into infinite
1079
+ * re-renders. Module-level on purpose.
1080
+ */
1081
+ const EMPTY_PENDING_MAP = new Map();
1082
+ /** 0.1.1 兜底:恒返回空 Map 的替身 hook(见组件里 useSessionPendingInteraction ?? EMPTY_PENDING_HOOK)。 */
1083
+ const EMPTY_PENDING_HOOK = (sel) => sel(EMPTY_PENDING_MAP);
888
1084
  /** Swipe geometry: reveal width of the archive action / settle threshold. */
889
1085
  const SWIPE_REVEAL = 88;
890
1086
  const SWIPE_SETTLE = 44;
@@ -1029,19 +1225,20 @@ function relativeTime(at) {
1029
1225
  /**
1030
1226
  * Card status subline (real-device round 2 follow-up, 2026-08-17): reuses
1031
1227
  * `dotState`'s own state read (the same ongoing/warning/done semantics the
1032
- * dot already encodes) plus `agentPreset` from the same `SessionSummary`
1033
- * row — no new data source. Returns undefined when there is neither a
1228
+ * dot already encodes) plus `agentPreset` from the same session row — no
1229
+ * new data source. Returns undefined when there is neither a
1034
1230
  * state nor a preset to show, so the caller can skip the subline entirely
1035
1231
  * rather than render an empty row.
1036
1232
  */
1037
1233
  function statusLine(row, dot, t) {
1234
+ const preset = (0, types_ts_1.agentPresetOf)(row);
1038
1235
  const state = dot === 'ongoing' ? t('homeStatusOngoing')
1039
1236
  : dot === 'warning' ? t('homeStatusWarning')
1040
1237
  : dot === 'done' ? t('homeStatusDone')
1041
1238
  : undefined;
1042
- if (state !== undefined && row.agentPreset !== undefined)
1043
- return `${state} · ${row.agentPreset}`;
1044
- return state ?? row.agentPreset;
1239
+ if (state !== undefined && preset !== undefined)
1240
+ return `${state} · ${preset}`;
1241
+ return state ?? preset;
1045
1242
  }
1046
1243
  /**
1047
1244
  * Phone home screen: the full-screen session list that owns the first level
@@ -1052,7 +1249,7 @@ function statusLine(row, dot, t) {
1052
1249
  * all navigation from the injected official actions; nothing here reads the
1053
1250
  * official DOM.
1054
1251
  */
1055
- function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession, startSession, downloadSessionLog, archiveSession, t, }) {
1252
+ function MobileHome({ useSessions, useWorkspaces, useSessionPendingInteraction, useStore, actions, openSession, startSession, downloadSessionLog, archiveSession, t, }) {
1056
1253
  const phone = usePhone();
1057
1254
  const iconHref = useSiteIconHref();
1058
1255
  const [iconBroken, setIconBroken] = (0, react_1.useState)(false);
@@ -1065,6 +1262,11 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
1065
1262
  const [sheet, setSheet] = (0, react_1.useState)(null);
1066
1263
  /** The one row whose archive action is swiped open (null = none). */
1067
1264
  const [swipeOpen, setSwipeOpen] = (0, react_1.useState)(null);
1265
+ // 0.1.2 的待处理交互来源;0.1.1 没有这个标准 prop,读到 undefined 时用一个
1266
+ // 恒返回空 Map 的替身,保证 hook 调用次数与顺序在两版之间一致(hooks 规则:
1267
+ // 真实 hook 只在 0.1.2 被调用,且每次渲染同一位置无条件调用一次)。
1268
+ const pendingHook = useSessionPendingInteraction ?? EMPTY_PENDING_HOOK;
1269
+ const pending = pendingHook((m) => m);
1068
1270
  /** Archive with the same confirm gate the session-info card uses. */
1069
1271
  const onArchiveRow = (id) => {
1070
1272
  if (!window.confirm(t('infoArchiveConfirm'))) {
@@ -1164,7 +1366,7 @@ function MobileHome({ useSessions, useWorkspaces, useStore, actions, openSession
1164
1366
  return null;
1165
1367
  const title = selectedWorkspace?.title ?? t('allWorkspaces');
1166
1368
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [view === 'session' && ((0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "hero-back", "aria-label": t('backToList'), onClick: () => actions.show('home'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconChevronLeftOutline14, { size: 20 }) })), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home", "data-view": view, "aria-hidden": view === 'session', children: [(0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "home-top", children: [iconHref !== undefined && !iconBroken && ((0, jsx_runtime_1.jsx)("img", { src: iconHref, alt: "", "aria-hidden": "true", "data-mobile-nav": "home-logo", onError: () => setIconBroken(true) })), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "ws-switch", "aria-haspopup": "menu", onClick: () => setSheet('filter'), children: [(0, jsx_runtime_1.jsx)("span", { children: title }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconChevronDownOutline14, { size: 14 })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "home-settings", "aria-label": t('settings'), title: t('settings'), onClick: openSettings, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconSettingsOutline16, { size: 18 }) })] }), (0, jsx_runtime_1.jsx)(MobileHomeChips_tsx_1.MobileHomeChips, { t: t, sessionId: sessions.current, downloadSessionLog: (id) => downloadSessionLog(id), onCustomize: () => setSheet('chips') }), (0, jsx_runtime_1.jsxs)("ul", { "data-mobile-nav": "home-list", children: [rows.map((row) => {
1167
- const dot = (0, session_dot_ts_1.dotState)(row);
1369
+ const dot = (0, session_dot_ts_1.dotState)(row, pending.has(row.id));
1168
1370
  const status = statusLine(row, dot, t);
1169
1371
  const initial = row.displayTitle.trim().charAt(0).toUpperCase();
1170
1372
  return ((0, jsx_runtime_1.jsx)(SwipeRow, { open: swipeOpen === row.id, onOpenChange: (open) => setSwipeOpen(open ? row.id : null), onArchive: () => onArchiveRow(row.id), archiveLabel: t('infoArchive'), children: (guardClick) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "home-row", "data-current": row.id === sessions.current ? '' : undefined, onClick: () => guardClick(() => enter(() => openSession(row.id))), children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-avatar", "aria-hidden": "true", children: dot !== undefined ? (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.StateDot, { state: dot, size: 10 }) : initial }), (0, jsx_runtime_1.jsxs)("span", { "data-mobile-nav": "home-row-body", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-title", children: row.displayTitle }), status !== undefined && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "home-row-status", children: status }))] }), (0, jsx_runtime_1.jsx)("time", { "data-mobile-nav": "home-row-time", dateTime: new Date(row.updatedAt).toISOString(), children: relativeTime(row.updatedAt) })] })) }, row.id));
@@ -1419,7 +1621,8 @@ exports.MobileSessionInfo = MobileSessionInfo;
1419
1621
  const jsx_runtime_1 = require("react/jsx-runtime");
1420
1622
  const react_1 = require("react");
1421
1623
  const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
1422
- const client_1 = require("@deepseek-ai/dsh-client-runtime/client");
1624
+ const store_ts_1 = require("./compat/store.js");
1625
+ const types_ts_1 = require("./compat/types.js");
1423
1626
  const nav_store_ts_1 = require("./nav-store.js");
1424
1627
  const history_nav_ts_1 = require("./history-nav.js");
1425
1628
  /** Layer id for the info sheet, so Android back closes it before leaving the session. */
@@ -1566,7 +1769,8 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
1566
1769
  };
1567
1770
  const cacheHit = usage === undefined ? null : cacheHitPercent(usage);
1568
1771
  const tokensEmpty = usage === undefined || (billedInputTokens(usage) === 0 && usage.outputTokens === 0);
1569
- const cwd = row?.cwd === undefined || row.cwd === '' ? undefined : (0, client_1.workspaceTitleOf)(row.cwd);
1772
+ const cwd = row?.cwd === undefined || row.cwd === '' ? undefined : (0, store_ts_1.workspaceTitleOf)(row.cwd);
1773
+ const preset = (0, types_ts_1.agentPresetOf)(row);
1570
1774
  const cells = [
1571
1775
  { label: t('infoStatTurns'), value: stats === undefined ? NA : String(stats.turns), sub: undefined },
1572
1776
  { label: t('infoStatSteps'), value: stats === undefined ? NA : String(stats.steps), sub: undefined },
@@ -1604,7 +1808,7 @@ function MobileSessionInfo({ sessionId, useSessions, useProjection, forkSession,
1604
1808
  if (!tab.active)
1605
1809
  tab.el.click();
1606
1810
  close();
1607
- }, children: tab.label }, tab.label))) })), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "info-close", "aria-label": t('infoClose'), onClick: close, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconCloseOutline16, { size: 16 }) })] }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-badges", children: [row?.agentPreset !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: row.agentPreset }), subagentCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoSubagents', { count: subagentCount }) })), jobCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoJobs', { count: jobCount }) })), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge-cwd", children: cwd ?? t('infoCwdFallback') })] }), (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-stats", children: cells.map((cell) => ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-stat", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-value", children: cell.value }), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-label", children: cell.label }), cell.sub !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-sub", children: cell.sub })] }, cell.label))) }), error !== null && (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-error", children: t('infoActionError', { message: error }) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onExport, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconDownloadOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoExport') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onRename, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconEditOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoRename') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onFork, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconBranchOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoFork') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", "data-mobile-nav-danger": "", disabled: busy, onClick: onArchive, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconArchiveOutline20, { size: 20 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoArchive') })] })] })] })] }));
1811
+ }, children: tab.label }, tab.label))) })), (0, jsx_runtime_1.jsx)("button", { type: "button", "data-mobile-nav": "info-close", "aria-label": t('infoClose'), onClick: close, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconCloseOutline16, { size: 16 }) })] }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-badges", children: [preset !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: preset }), subagentCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoSubagents', { count: subagentCount }) })), jobCount > 0 && ((0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge", children: t('infoJobs', { count: jobCount }) })), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-badge-cwd", children: cwd ?? t('infoCwdFallback') })] }), (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-stats", children: cells.map((cell) => ((0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-stat", children: [(0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-value", children: cell.value }), (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-label", children: cell.label }), cell.sub !== undefined && (0, jsx_runtime_1.jsx)("span", { "data-mobile-nav": "info-stat-sub", children: cell.sub })] }, cell.label))) }), error !== null && (0, jsx_runtime_1.jsx)("div", { "data-mobile-nav": "info-error", children: t('infoActionError', { message: error }) }), (0, jsx_runtime_1.jsxs)("div", { "data-mobile-nav": "info-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onExport, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconDownloadOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoExport') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onRename, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconEditOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoRename') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", disabled: busy, onClick: onFork, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconBranchOutline16, { size: 16 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoFork') })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "data-mobile-nav": "info-action", "data-mobile-nav-danger": "", disabled: busy, onClick: onArchive, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.IconArchiveOutline20, { size: 20 }), (0, jsx_runtime_1.jsx)("span", { children: t('infoArchive') })] })] })] })] }));
1608
1812
  }
1609
1813
  };
1610
1814
  __modules["attach-upload.js"] = function (require, module, exports) {
@@ -3470,22 +3674,39 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
3470
3674
 
3471
3675
  /* ---------- dsh-vision-router: composer vision toggle (2026-08-26) ----------
3472
3676
  THIRD-PARTY COMPAT RULE — dsh-vision-router registers a Vision mode
3473
- toggle into conversation.input.right (lib/client-presentation-boundary-
3474
- main.js, slot id "vision-router-mode-toggle"): a bordered pill with
3475
- THREE text spans (an eye emoji, the localized label, and a check mark
3476
- while active) sized by INLINE styles — minHeight 28, padding 4px 8px,
3477
- fontSize 12, white-space nowrap — roughly 90px of unshrinkable width.
3677
+ toggle into conversation.input.right (slot id
3678
+ "vision-router-mode-toggle"): a bordered pill sized by INLINE styles —
3679
+ minHeight 28, padding 4px 8px, fontSize 12, white-space nowrap —
3680
+ roughly 90px of unshrinkable width.
3681
+
3682
+ Its children changed shape in 2.1.x (re-measured on the real UI
3683
+ 2026-09-04, plugin 2.1.1). Both forms are handled:
3684
+ - text form (<= 2.0.x): <span>👁</span><span>label</span> plus a
3685
+ <span>✓</span> while active;
3686
+ - svg form (2.1.x, lib/client-presentation-boundary.js): a 14px eye
3687
+ <svg>, <span>label</span>, plus a SECOND 12px check <svg> while
3688
+ active.
3689
+ The 2.1.1 update is what the user hit: the label span was still hidden
3690
+ but the new eye svg was not, so it rendered NEXT TO our ::before eye —
3691
+ two eyes in a 28px button — and the active check svg had nothing hiding
3692
+ it at all, eating width the phone row does not have.
3478
3693
 
3479
3694
  The phone composer row is nowrap by design (composer.css.ts section 1)
3480
3695
  and the model seat is its only shrinkable item, so that pill squeezes
3481
3696
  the model name out of the row (user report, 2026-08-26).
3482
3697
 
3483
3698
  Phone treatment: a 28x28 round icon button in the attach button's
3484
- language (same size, same neutral fill). All three spans are hidden and
3485
- ONE 16px eye is drawn by a CSS mask on ::before with currentColor — the
3486
- same survives-React-re-render reasoning as the auto-approve shield
3487
- below: the button re-renders on every state flip, so a DOM-injected
3488
- icon would need an observer while generated content does not. The
3699
+ language (same size, same neutral fill), showing exactly ONE 16px eye
3700
+ and nothing else. Every span is hidden (label, and the text form's
3701
+ emoji and check); of the svg form's icons only the first survives, the
3702
+ trailing check svg is hidden by a sibling combinator so the count is
3703
+ never assumed. Our own ::before eye is now a FALLBACK, gated on
3704
+ :has(svg) so it draws only for the text form — the plugin ships its own
3705
+ icon from 2.1.x on, and drawing over it is what produced the double
3706
+ eye. Generated content rather than a DOM-injected icon, for the same
3707
+ survives-React-re-render reason as the auto-approve shield below: the
3708
+ button re-renders on every state flip, so an injected node would need
3709
+ an observer while ::before does not. The
3489
3710
  accessible name is unaffected: the button ships aria-label + title with
3490
3711
  the full enable/disable copy, and aria-pressed keeps conveying the
3491
3712
  state; visually the plugin's own inline styles still flip border +
@@ -3528,12 +3749,27 @@ exports.COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
3528
3749
  background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
3529
3750
  box-shadow: none !important;
3530
3751
  }
3531
- /* Eye emoji + label + active check: all three spans out. */
3752
+ /* Text form: emoji + label + active check are all spans — all out. */
3532
3753
  [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > span {
3533
3754
  display: none !important;
3534
3755
  }
3756
+ /* Svg form (2.1.x): the first svg IS the eye, so keep it and match the
3757
+ row's other icons at 16px. */
3758
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > svg:first-of-type {
3759
+ width: 16px !important;
3760
+ height: 16px !important;
3761
+ }
3762
+ /* ...and drop every svg after it — today that is the active-state check
3763
+ mark. A sibling combinator rather than :nth-child(2): the count is the
3764
+ plugin's business, this only says "one icon, the first one". */
3765
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > svg ~ svg {
3766
+ display: none !important;
3767
+ }
3535
3768
  /* The one svg: eye outline + iris as a currentColor mask — brand while
3536
3769
  active, dimmed with the button by the plugin's own disabled opacity. */
3770
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:has(svg)::before {
3771
+ content: none !important;
3772
+ }
3537
3773
  [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]::before {
3538
3774
  content: '';
3539
3775
  width: 16px;
@@ -4847,12 +5083,28 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4847
5083
  }
4848
5084
  ${ROW} > [class$="_tools"] > [class$="_modes"] {
4849
5085
  order: 3 !important;
4850
- flex: 0 1 auto !important;
4851
- min-width: 0 !important;
5086
+ /* Rigid on purpose. This seat holds the permission chip, whose own
5087
+ wrapper (the plugin's element inside _modes) is \`flex: 0 0 auto\` and
5088
+ will NOT follow the seat down — so a shrinkable seat does not make the
5089
+ chip narrower, it just slices the seat out from under a chip that
5090
+ stays 44px wide, and the overflow lands on top of the model pill.
5091
+ Measured 2026-09-04 on DSH 0.1.2 at 390px: seat 90-120 (29.7px) with
5092
+ the chip still 90-134, model starting at 125 — a 9px overlap, exactly
5093
+ the "the two chips touch" report. With the seat rigid: seat 90-134,
5094
+ model 139-246, overlap 0; and at 320px the model pill shrinks to 52px
5095
+ and still nothing overlaps.
5096
+ This also restores what section 1 says out loud — the model pill is
5097
+ the ONLY shrinkable item in the row. \`min-width: 0\` is gone with the
5098
+ shrink: it only ever mattered as the companion of \`flex: 0 1\`. */
5099
+ flex: 0 0 auto !important;
4852
5100
  gap: 6px !important;
4853
5101
  }
4854
- /* The model seat carries the elastic gap: everything after it is pushed to
4855
- the right edge, so no spacer element is needed. */
5102
+ /* The elastic gap used to live here as \`margin-right: auto\`, which broke
5103
+ the moment a session had no model pill: a subagent session has none, so
5104
+ the gap vanished and the stop / send buttons bunched up in the middle of
5105
+ the row instead of sitting at the right edge (reported 2026-09-06). The
5106
+ gap belongs to the RIGHT group, not to the last item of the left one —
5107
+ see the two rules below the order list. */
4856
5108
  ${MODEL} {
4857
5109
  order: 4 !important;
4858
5110
  /* A definite flex-basis (not auto) breaks the circular sizing between
@@ -4863,7 +5115,6 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4863
5115
  flex: 0 1 min(48vw, 200px) !important;
4864
5116
  min-width: 0 !important;
4865
5117
  max-width: min(48vw, 200px) !important;
4866
- margin-right: auto !important;
4867
5118
  }
4868
5119
  /* Third-party input.right entries park next to the ring rather than
4869
5120
  landing at order 0 (= far left) once the groups are flattened. */
@@ -4878,6 +5129,22 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4878
5129
  ${ROW} > [class$="_trailing"] > [class$="_primary"] {
4879
5130
  order: 7 !important;
4880
5131
  }
5132
+ /* The elastic gap. It sits in front of whichever of the two right-hand
5133
+ items comes first, so the group hugs the right edge no matter what the
5134
+ left side happens to contain — with a model pill, without one (subagent
5135
+ sessions), with or without the third-party entries at order 5.
5136
+ Both get \`margin-left: auto\` and the last rule takes it back off the
5137
+ send button whenever a ring precedes it: two auto margins would SHARE
5138
+ the free space and open a second gap between the ring and send. A
5139
+ sibling combinator answers "is there a ring before me" without assuming
5140
+ anything about the host's nesting. */
5141
+ ${ROW} > [class$="_trailing"] > span[class$="_root"],
5142
+ ${ROW} > [class$="_trailing"] > [class$="_primary"] {
5143
+ margin-left: auto !important;
5144
+ }
5145
+ ${ROW} > [class$="_trailing"] > span[class$="_root"] ~ [class$="_primary"] {
5146
+ margin-left: 0 !important;
5147
+ }
4881
5148
 
4882
5149
  /* --- 3. permission as icon-only pill; model as name+effort pill ---
4883
5150
  (real-device round 2, 2026-08-17; model pill reworked 2026-08-25:
@@ -4998,6 +5265,115 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
4998
5265
  border-radius: 12px !important;
4999
5266
  font-size: 15px !important;
5000
5267
  }
5268
+ /* --- 4a. third-party composer entries live in the model sheet ---
5269
+ effects/model-sheet-extras.ts parks the \`conversation.input.right\`
5270
+ container inside the model menu while that menu is open and puts it back
5271
+ when it closes; these rules are the two halves of that trip.
5272
+
5273
+ In the row: hidden, but ONLY once the effect has marked the container as
5274
+ one it manages. Keying the hide off the marker rather than off "not
5275
+ currently parked" is what makes the failure direction safe — if the
5276
+ effect declines to act (nothing in the slot but the vision toggle, so
5277
+ nothing worth moving) or never runs at all, the marker is absent, this
5278
+ rule does not match, and the controls stay exactly where the host put
5279
+ them. The inverse spelling hid them in the row whenever they were not in
5280
+ the sheet, which in those same cases left them reachable from neither
5281
+ place (caught in review, 2026-09-06).
5282
+ Hiding by ancestor rather than by a display rule on the container itself
5283
+ is what lets the parked copy style itself freely — same element, two
5284
+ homes, two rule sets. \`!important\` because the slot container carries an
5285
+ inline \`display: contents\`. */
5286
+ ${ROW} > [class$="_trailing"] > [data-slot="conversation.input.right"][data-zen-sheet-extras] {
5287
+ display: none !important;
5288
+ }
5289
+ /* Parked in the sheet: a column of full-width rows in the sheet's own
5290
+ language, so a relocated control reads as a sibling of 模型 and 推理等级
5291
+ rather than a chip that wandered in. */
5292
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] {
5293
+ display: flex !important;
5294
+ flex-direction: column !important;
5295
+ gap: 2px !important;
5296
+ width: 100% !important;
5297
+ }
5298
+ /* Undo the row's compaction on every parked control: back to full width and
5299
+ the 48px the sheet's own cells use. \`> * > *\` reaches both shapes without
5300
+ naming either plugin — subscriptions wraps its trigger in a positioning
5301
+ div, vision-router puts its button straight in the slot. */
5302
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > *,
5303
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > * > button {
5304
+ width: 100% !important;
5305
+ max-width: none !important;
5306
+ min-width: 0 !important;
5307
+ justify-content: flex-start !important;
5308
+ }
5309
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] button {
5310
+ box-sizing: border-box !important;
5311
+ min-height: 48px !important;
5312
+ height: auto !important;
5313
+ padding: 0 12px !important;
5314
+ gap: 10px !important;
5315
+ border-radius: 12px !important;
5316
+ border: none !important;
5317
+ background: transparent !important;
5318
+ font-size: 15px !important;
5319
+ /* Both plugins center their chip label, which is right for a pill in the
5320
+ row and wrong for a full-width row: the sheet's own cells start their
5321
+ label at the left edge. justify-content covers the flex triggers,
5322
+ text-align the ones that are not flex containers. */
5323
+ text-align: left !important;
5324
+ justify-content: flex-start !important;
5325
+ }
5326
+ /* Order comes from the row (compat.css.ts puts the vision toggle at 5 so it
5327
+ parks beside the context ring), and that rule still matches in here — the
5328
+ sheet is a DOM descendant of the composer bar even though it paints as a
5329
+ fixed layer. Reset it so the sheet follows DOM order instead of
5330
+ inheriting a decision that was about a different layout. */
5331
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > * {
5332
+ order: 0 !important;
5333
+ }
5334
+ /* The vision toggle is squeezed to a 28px icon in the row (compat.css.ts);
5335
+ in the sheet it is a row, so its label comes back and the icon leads. */
5336
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > span {
5337
+ display: inline !important;
5338
+ }
5339
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > svg:first-of-type {
5340
+ width: 18px !important;
5341
+ height: 18px !important;
5342
+ }
5343
+ /* The speed chip opens a menu of its own, anchored \`bottom: 100%\` to its
5344
+ trigger. Inside the sheet — which is \`overflow: hidden\` so its rounded
5345
+ top clips its content — that menu would be cut off. Same escape section 4
5346
+ uses for the host's two menus: position: fixed leaves the clip behind
5347
+ (the sheet sets \`transform: none\`, so nothing between here and the
5348
+ viewport captures fixed positioning) and it lands as its own sheet on top
5349
+ of this one. */
5350
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [role="menu"] {
5351
+ position: fixed !important;
5352
+ left: 0 !important;
5353
+ right: 0 !important;
5354
+ bottom: 0 !important;
5355
+ top: auto !important;
5356
+ margin: 0 !important;
5357
+ width: 100% !important;
5358
+ min-width: 0 !important;
5359
+ max-width: none !important;
5360
+ max-height: min(70dvh, 520px) !important;
5361
+ box-sizing: border-box !important;
5362
+ border-radius: 16px 16px 0 0 !important;
5363
+ border-bottom: none !important;
5364
+ padding: 8px 8px calc(8px + var(--mnav-sab)) !important;
5365
+ /* Above the model sheet it sits on (60, section 4), below the session-info
5366
+ sheet's 70 (info.css.ts) — it is a child of the model sheet, not a peer
5367
+ of the page's own layers. */
5368
+ z-index: 61 !important;
5369
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;
5370
+ }
5371
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [role="menu"] [role="menuitemradio"] {
5372
+ min-height: 48px !important;
5373
+ border-radius: 12px !important;
5374
+ font-size: 15px !important;
5375
+ }
5376
+
5001
5377
  /* --- 4b. the official scroll-to-bottom button must not poke through the
5002
5378
  sheet (real-device follow-up, 2026-08-17) ---
5003
5379
  ChatView's own "jump to latest" button (aria-label t("chat.toBottom"))
@@ -5032,6 +5408,17 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
5032
5408
  [class$="_composerSeat"] {
5033
5409
  --dsh-composer-text-max-height: 124px;
5034
5410
  }
5411
+ /* Pull the text in from the card's left border. Upstream insets the editing
5412
+ host by 16px while the control row below it starts 8px in (row padding,
5413
+ section 1), so the placeholder sat noticeably further out than the
5414
+ buttons under it and the field read as over-padded on a phone (reported
5415
+ 2026-09-06). 12px squares it with the 12px of clear space on the right,
5416
+ giving the text a symmetric box, and closes most of the gap with the
5417
+ button row. Anchored on the editing host's own contract attribute rather
5418
+ than a hashed class. */
5419
+ [data-slot="conversation.composer.bar"] [data-composer-input] {
5420
+ padding-left: 12px !important;
5421
+ }
5035
5422
 
5036
5423
  /* --- 6. no divider above OR below the message list ---
5037
5424
  Instead of a rule the messages butt against, they fade near both edges.
@@ -5123,7 +5510,13 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
5123
5510
  composer. Anchored on the official data-testid, a stable contract
5124
5511
  marker rather than a hashed class name. */
5125
5512
  [data-slot="conversation.input.dock"] > [data-testid="todo-panel"] {
5126
- flex: 1 1 auto !important;
5513
+ /* A whole line to itself. It shared line 0 with the pills until the goal
5514
+ bar turned up (2026-09-06): two full-width things on one nowrap line
5515
+ meant the to-do header shrank to a stub and the goal bar ran off the
5516
+ right edge. Both are status strips you read at a glance, so they stack
5517
+ — see the line policy in 7c. */
5518
+ flex: 1 0 100% !important;
5519
+ order: 1 !important;
5127
5520
  min-width: 0 !important;
5128
5521
  max-width: none !important;
5129
5522
  min-height: 0 !important;
@@ -5150,7 +5543,7 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
5150
5543
  display: flex !important;
5151
5544
  flex: 1 0 100% !important;
5152
5545
  flex-wrap: wrap !important;
5153
- order: 2 !important;
5546
+ order: 4 !important;
5154
5547
  align-items: center !important;
5155
5548
  gap: 6px !important;
5156
5549
  max-width: none !important;
@@ -5185,12 +5578,12 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
5185
5578
  sits closest to the input it will be sent from. The rail only wraps
5186
5579
  while one of the two full-width entries exists, so a session with
5187
5580
  neither keeps the original single scrolling chip line. */
5188
- [data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"]) {
5581
+ [data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"], > [data-testid="todo-panel"], > [data-goal-bar]) {
5189
5582
  flex-wrap: wrap !important;
5190
5583
  }
5191
5584
  [data-slot="conversation.input.dock"] > [data-queue-dock] {
5192
5585
  flex: 1 0 100% !important;
5193
- order: 1 !important;
5586
+ order: 3 !important;
5194
5587
  max-width: none !important;
5195
5588
  min-height: 0 !important;
5196
5589
  max-height: none !important;
@@ -5201,6 +5594,24 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
5201
5594
  font-size: 13px !important;
5202
5595
  line-height: normal !important;
5203
5596
  }
5597
+ /* dsh-client-ui-goal's status strip (its own \`data-goal-bar\` marker; it
5598
+ registers into this dock at order 10). Fourth thing here that is not a
5599
+ pill: a row carrying the objective text plus pause / edit / delete
5600
+ buttons, so the 26px cage clipped its buttons off the right edge and the
5601
+ nowrap line pushed what was left off screen (reported 2026-09-06). Same
5602
+ treatment the queue gets — a line of its own, official geometry back. */
5603
+ [data-slot="conversation.input.dock"] > [data-goal-bar] {
5604
+ flex: 1 0 100% !important;
5605
+ order: 2 !important;
5606
+ max-width: none !important;
5607
+ min-height: 0 !important;
5608
+ max-height: none !important;
5609
+ border-radius: 12px !important;
5610
+ overflow: visible !important;
5611
+ font-size: 13px !important;
5612
+ line-height: normal !important;
5613
+ }
5614
+
5204
5615
  /* The panel is the dock's only child; upstream rounds its top corners only
5205
5616
  and drops the bottom border, because there it is half-hidden behind the
5206
5617
  card. On its own line it is a free-standing strip, so close it up. */
@@ -5466,6 +5877,27 @@ exports.INFO_CSS = `/* ---------- session-info sheet (< 768px) ---------- */
5466
5877
  display: block !important;
5467
5878
  }
5468
5879
 
5880
+ /* The sheet renders inside the header, and \`[data-phase] header\` is a
5881
+ stacking context of its own (position: relative + z-index: 2, set in
5882
+ header.css.ts so the header's fade strip paints over the scroller). A
5883
+ child's z-index is scoped to that context, so the z:70 below competes not
5884
+ with the page but with the header's own 2 — and the host's composer seat
5885
+ (.wSkVaW_composerSeat, z:7, from dsh-client-ui-conversation) outranks
5886
+ it. Measured live on DSH 0.1.2, 2026-09-04: with the sheet open, the
5887
+ topmost element over the composer band was the composer's own trigger
5888
+ button, i.e. the input row painted THROUGH the card.
5889
+ Promote the header itself for exactly as long as the sheet is mounted —
5890
+ MobileSessionInfo returns null when closed (\`if (!open) return null\`),
5891
+ so the header drops back to 2 on close (verified: 70 while open, 2
5892
+ after). Descendant :has(), never \`>\`: the utilities slot wrapper is
5893
+ \`display: contents\` and the host owns the depth (AGENTS.md — do not
5894
+ encode host DOM depth in a selector).
5895
+ Not raising the seat's own z-index instead: it belongs to the host and
5896
+ is shared with the scroll-to-bottom button and the composer sheets. */
5897
+ [data-phase] header:has([data-mobile-nav="info-layer"]) {
5898
+ z-index: 70;
5899
+ }
5900
+
5469
5901
  [data-mobile-nav="info-layer"] {
5470
5902
  position: fixed;
5471
5903
  inset: 0;
@@ -7677,14 +8109,35 @@ function installHeaderStatusDot(ctx) {
7677
8109
  return;
7678
8110
  const { current, byId } = ctx.sessions.list.getSnapshot();
7679
8111
  const row = current === undefined ? undefined : byId[current];
7680
- const state = row === undefined ? undefined : (0, session_dot_ts_1.dotState)(row);
8112
+ // 0.1.2 的待处理交互来源,每次现查:本插件的 inject 不含 uiSession(加了
8113
+ // 0.1.1 就不激活),apply 此刻服务未必注册好;每次算 dot 时现查,注册顺序
8114
+ // 就不再是问题。0.1.1 上没有这个服务,这里恒 undefined,黄点只由行字段决定。
8115
+ const uiSession = ctx.get('uiSession');
8116
+ const pending = current === undefined ? false
8117
+ : uiSession?.pendingInteractions.getSnapshot().has(current) === true;
8118
+ const state = row === undefined ? undefined : (0, session_dot_ts_1.dotState)(row, pending);
7681
8119
  if (state === undefined)
7682
8120
  frame.removeAttribute('data-mobile-nav-dot');
7683
8121
  else
7684
8122
  frame.setAttribute('data-mobile-nav-dot', state);
7685
8123
  };
7686
8124
  apply();
7687
- return ctx.sessions.list.subscribe(apply);
8125
+ // Two sources can push the dot now (session list + the 0.1.2 pending
8126
+ // map); probe uiSession once more right here and subscribe to whichever
8127
+ // exist. 0.1.1 only ever has the first. Degradation note: if uiSession
8128
+ // has not registered by this moment (this plugin ran before it), only
8129
+ // the list subscription is attached — the dot stays correct because
8130
+ // every later list change re-runs apply(), whose fresh probe then sees
8131
+ // the service; a pending-only change before any list change would be
8132
+ // missed until then.
8133
+ const unsubscribes = [ctx.sessions.list.subscribe(apply)];
8134
+ const pendingSource = ctx.get('uiSession')?.pendingInteractions;
8135
+ if (pendingSource !== undefined)
8136
+ unsubscribes.push(pendingSource.subscribe(apply));
8137
+ return () => {
8138
+ for (const off of unsubscribes)
8139
+ off();
8140
+ };
7688
8141
  }, 'dsh-mobile-nav: header status dot');
7689
8142
  }
7690
8143
  };
@@ -8049,6 +8502,166 @@ function installModalBack(ctx) {
8049
8502
  }, 'dsh-zen-remote: modal back gesture');
8050
8503
  }
8051
8504
  };
8505
+ __modules["effects/model-sheet-extras.js"] = function (require, module, exports) {
8506
+ "use strict";
8507
+ Object.defineProperty(exports, "__esModule", { value: true });
8508
+ exports.installModelSheetExtras = installModelSheetExtras;
8509
+ /** Phone breakpoint — same query every phone-only effect in this plugin uses. */
8510
+ const PHONE_QUERY = '(max-width: 767px)';
8511
+ /** The model pill itself. Absent in a subagent session — see `sync`. */
8512
+ const MODEL_SEAT_SELECTOR = '[data-slot="conversation.input.model"]';
8513
+ /** The model pill's own popup, which section 4 of composer.css.ts turns into a bottom sheet. */
8514
+ const MENU_SELECTOR = '[data-slot="conversation.input.model"] [class$="_menu"]';
8515
+ /**
8516
+ * The third-party composer controls this effect relocates. Both register into
8517
+ * `conversation.input.right`; moving the slot container rather than its
8518
+ * children keeps them together and leaves exactly one node to put back.
8519
+ */
8520
+ const EXTRAS_SELECTOR = '[data-slot="conversation.input.right"]';
8521
+ /**
8522
+ * dsh-vision-router's own mount marker. Used here only to answer "is there
8523
+ * anything in this slot BESIDES the vision toggle" — see `worthMoving`.
8524
+ */
8525
+ const VISION_SELECTOR = '[data-vision-router-mode-toggle]';
8526
+ /**
8527
+ * Set on the container while this effect manages it, in the row and in the
8528
+ * sheet alike. It is the styling hook for both halves in composer.css.ts, and
8529
+ * it is deliberately what the row's `display: none` keys off: without the
8530
+ * marker the controls stay exactly where the host put them, so a version of
8531
+ * this file that declines to act — or never runs at all — cannot leave them
8532
+ * hidden in the row and absent from the sheet, i.e. unreachable.
8533
+ */
8534
+ const MANAGED = 'data-zen-sheet-extras';
8535
+ /**
8536
+ * Is the slot holding anything that actually costs the row width?
8537
+ *
8538
+ * The row is one nowrap flex line whose only elastic item is the model pill
8539
+ * (composer.css.ts section 1), so entries here are paid for out of the model
8540
+ * name. But that is only a problem once something WIDE lands in the slot —
8541
+ * today dsh-plugin-subscriptions' speed chip, which renders "速度 · 标准" and
8542
+ * eats ~70px. dsh-vision-router's toggle alone is a 28px icon that the row
8543
+ * accommodates fine, and moving it on its own would cost a tap (it becomes
8544
+ * two: open the sheet, then toggle) to buy width nobody needed.
8545
+ *
8546
+ * So: relocate only when the slot holds an entry that is not the vision
8547
+ * toggle. With no such entry — subscriptions not installed, or installed but
8548
+ * not contributing on this model — nothing moves and the row keeps its one
8549
+ * icon, which is the behaviour asked for (user review, 2026-09-06).
8550
+ *
8551
+ * Why this rather than sniffing for subscriptions by name: the slot system
8552
+ * adds no per-registration marker (measured — entries render straight into the
8553
+ * container) and subscriptions marks nothing of its own, so naming it would
8554
+ * mean adding a marker to our fork of that plugin and anchoring on it. That
8555
+ * marker dies the day the fork is dropped, and the feature would go quiet
8556
+ * with nothing to catch it. The width question is the real question anyway,
8557
+ * and this asks it directly.
8558
+ */
8559
+ function worthMoving(extras) {
8560
+ return [...extras.children].some(child => !child.matches(VISION_SELECTOR));
8561
+ }
8562
+ /**
8563
+ * Phone: park the composer row's third-party controls inside the model sheet.
8564
+ *
8565
+ * With the speed chip and the vision toggle both present on a GPT model the
8566
+ * row runs out of width. Both are model-scoped settings, so the model sheet —
8567
+ * which already holds 模型 and 推理等级 — is where they belong (user request,
8568
+ * 2026-09-06).
8569
+ *
8570
+ * Why move the real controls instead of drawing our own rows that drive them:
8571
+ * the same reason native-trigger-overlay.ts moves the real trigger. A stand-in
8572
+ * has to script the original, which is a road this plugin does not go down —
8573
+ * beyond the trusted-input wall that effect documents, our own rows would have
8574
+ * to mirror every piece of state (speed tier, disabled, aria-pressed, whether
8575
+ * a vision twin exists for the current model) and would rot on the next
8576
+ * upstream restyle. Moving the node keeps one source of truth: their React
8577
+ * roots go on owning and updating these elements, we only change where they
8578
+ * hang.
8579
+ *
8580
+ * Measured 2026-09-06 (DSH 0.1.2, 390px) before writing this — the two things
8581
+ * that would have killed the approach both hold: a relocated control survives
8582
+ * host re-renders (typing into the composer re-renders the row and does not
8583
+ * yank it back) and stays hit-testable at its new position.
8584
+ *
8585
+ * Restoring on close is not cosmetic. The sheet unmounts when it closes and
8586
+ * anything still inside goes with it, so the controls would vanish from the
8587
+ * page while their React roots kept updating detached nodes. The observer
8588
+ * therefore watches the menu leaving too, and the parked node is held as a
8589
+ * reference rather than looked up — once the menu is detached a document
8590
+ * query can no longer find it.
8591
+ */
8592
+ function installModelSheetExtras(ctx) {
8593
+ ctx.effect(() => {
8594
+ const phone = window.matchMedia(PHONE_QUERY);
8595
+ /** The container while parked in the sheet, plus where to put it back. */
8596
+ let parked = null;
8597
+ const park = (node, menu) => {
8598
+ if (parked !== null || node.parentElement === null)
8599
+ return;
8600
+ parked = { node, parent: node.parentElement, next: node.nextSibling };
8601
+ menu.appendChild(node);
8602
+ };
8603
+ const unpark = () => {
8604
+ if (parked === null)
8605
+ return;
8606
+ const { node, parent, next } = parked;
8607
+ parked = null;
8608
+ // The menu may already be detached, having taken the container with it.
8609
+ // The node itself is still live either way; re-home it only if its old
8610
+ // parent is still on the page (when the whole composer went away, the
8611
+ // owning React roots rebuild the slot on the next mount).
8612
+ if (parent.isConnected)
8613
+ parent.insertBefore(node, next);
8614
+ };
8615
+ /** Hand the container back to the host, marker and all. */
8616
+ const release = (node) => {
8617
+ unpark();
8618
+ node?.removeAttribute(MANAGED);
8619
+ };
8620
+ const sync = () => {
8621
+ const extras = document.querySelector(EXTRAS_SELECTOR);
8622
+ if (extras === null) {
8623
+ unpark();
8624
+ return;
8625
+ }
8626
+ // No model pill, no sheet to park into — a subagent session has none.
8627
+ // Claiming the slot there would hide the controls from the row with
8628
+ // nowhere to show them instead, which is the unreachable state the
8629
+ // marker is meant to prevent.
8630
+ const seat = document.querySelector(MODEL_SEAT_SELECTOR);
8631
+ if (!phone.matches || seat === null || !worthMoving(extras)) {
8632
+ release(extras);
8633
+ return;
8634
+ }
8635
+ extras.setAttribute(MANAGED, '');
8636
+ const menu = document.querySelector(MENU_SELECTOR);
8637
+ if (menu === null)
8638
+ unpark();
8639
+ else
8640
+ park(extras, menu);
8641
+ };
8642
+ // Synchronous in the observer callback, never behind rAF: the callback is
8643
+ // a microtask and runs before this frame paints, so the controls are in
8644
+ // the sheet the first time it is drawn. A rAF would land a frame late and
8645
+ // show one frame of the sheet without them (AGENTS.md's "晚一帧" rule).
8646
+ // Body-level childList is the same shape modal-back.ts uses; aria-expanded
8647
+ // is watched too so a close that reuses the node still re-syncs.
8648
+ const observer = new MutationObserver(sync);
8649
+ observer.observe(document.body, {
8650
+ childList: true,
8651
+ subtree: true,
8652
+ attributes: true,
8653
+ attributeFilter: ['aria-expanded'],
8654
+ });
8655
+ phone.addEventListener('change', sync);
8656
+ sync();
8657
+ return () => {
8658
+ observer.disconnect();
8659
+ phone.removeEventListener('change', sync);
8660
+ release(document.querySelector(EXTRAS_SELECTOR));
8661
+ };
8662
+ }, 'dsh-mobile-nav: model sheet extras');
8663
+ }
8664
+ };
8052
8665
  __modules["effects/native-trigger-overlay.js"] = function (require, module, exports) {
8053
8666
  "use strict";
8054
8667
  /**
@@ -8727,6 +9340,7 @@ const header_status_ts_1 = require("./effects/header-status.js");
8727
9340
  const gestures_ts_1 = require("./effects/gestures.js");
8728
9341
  const turn_fold_ts_1 = require("./effects/turn-fold.js");
8729
9342
  const modal_back_ts_1 = require("./effects/modal-back.js");
9343
+ const model_sheet_extras_ts_1 = require("./effects/model-sheet-extras.js");
8730
9344
  const native_trigger_overlay_ts_1 = require("./effects/native-trigger-overlay.js");
8731
9345
  const welcome_notice_ts_1 = require("./effects/welcome-notice.js");
8732
9346
  const keyboard_guard_ts_1 = require("./effects/keyboard-guard.js");
@@ -8779,6 +9393,9 @@ function apply(ctx) {
8779
9393
  (0, turn_fold_ts_1.installTurnFold)(ctx);
8780
9394
  (0, modal_back_ts_1.installModalBack)(ctx);
8781
9395
  (0, native_trigger_overlay_ts_1.installNativeTriggerOverlay)(ctx);
9396
+ // Third-party composer entries (speed chip, vision toggle) move into the
9397
+ // model sheet — the row has no width to spare and both are model settings.
9398
+ (0, model_sheet_extras_ts_1.installModelSheetExtras)(ctx);
8782
9399
  // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
8783
9400
  // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
8784
9401
  // per-browser "不再弹出" opt-out instead.
@@ -8845,7 +9462,13 @@ function apply(ctx) {
8845
9462
  forkSession: (id) => ctx.sessions.fork({ sessionId: id }),
8846
9463
  openSession: (id) => ctx.sessions.open(id),
8847
9464
  renameSession: (id, title) => ctx.sessions.binding(id)?.session.rename(title),
8848
- archiveSession: (id) => ctx.workspaces.archiveSession(id),
9465
+ // 0.1.2 的界面级归档在 uiWorkspace(顺带清当前选中);懒查——回调是用户
9466
+ // 点击才跑,那时服务必已注册。本插件的 inject 不包含 uiWorkspace(0.1.1
9467
+ // 没有它),所以启动时不能查一次就用:可能早于它注册。
9468
+ archiveSession: (id) => {
9469
+ const ui = ctx.get('uiWorkspace');
9470
+ return ui === undefined ? ctx.workspaces.archiveSession(id) : ui.archiveSession(id);
9471
+ },
8849
9472
  downloadSessionLog: (id) => ctx.sessionLogDownload.download(id),
8850
9473
  }),
8851
9474
  }, MobileSessionInfo_tsx_1.MobileSessionInfo));
@@ -8893,11 +9516,26 @@ function apply(ctx) {
8893
9516
  store: nav,
8894
9517
  inject: () => ({
8895
9518
  openSession: (id) => ctx.sessions.open(id),
8896
- startSession: (workspaceId) => ctx.workspaces.startSession(workspaceId),
9519
+ // uiWorkspace 懒查:回调是用户点击才跑,服务那时必已注册(inject 不含
9520
+ // uiWorkspace,启动时可能先于它注册,不能启动查一次就用)。0.1.1 上恒
9521
+ // undefined,退回 ctx.workspaces.startSession——0.1.1 专用分支,0.1.2 的
9522
+ // 类型里该方法已不存在,故对类型做 LegacyWorkspacesLike cast,运行时走不到。
9523
+ startSession: (workspaceId) => {
9524
+ const ui = ctx.get('uiWorkspace');
9525
+ return ui === undefined
9526
+ ? ctx.workspaces.startSession(workspaceId)
9527
+ : ui.startSession(workspaceId);
9528
+ },
8897
9529
  // S5 session-log chip — the same service call MobileDrawerFooter uses.
8898
9530
  downloadSessionLog: (id) => ctx.sessionLogDownload.download(id),
8899
- // Row swipe action — the same binding MobileSessionInfo archives with.
8900
- archiveSession: (id) => ctx.workspaces.archiveSession(id),
9531
+ // Row swipe action — the same binding MobileSessionInfo archives with;
9532
+ // on 0.1.2 the uiWorkspace variant also clears the current selection
9533
+ // when the archived session is the one selected. Same lazy lookup as
9534
+ // the session-info sheet's archive binding above.
9535
+ archiveSession: (id) => {
9536
+ const ui = ctx.get('uiWorkspace');
9537
+ return ui === undefined ? ctx.workspaces.archiveSession(id) : ui.archiveSession(id);
9538
+ },
8901
9539
  }),
8902
9540
  }, MobileHome_tsx_1.MobileHome));
8903
9541
  // Session log download, relocated from the session header to the drawer
@@ -8917,6 +9555,9 @@ function apply(ctx) {
8917
9555
  order: 5,
8918
9556
  locale: locales_ts_1.NS,
8919
9557
  inject: () => ({
9558
+ // The footer slot contract hands the id over as a loose string;
9559
+ // download() wants the branded SessionId — pure type-layer narrowing
9560
+ // (runtime value is a session id either way, same as MobileHome.tsx).
8920
9561
  downloadSessionLog: (sessionId) => ctx.sessionLogDownload.download(sessionId),
8921
9562
  toggleSidebar: () => ctx.layout.toggleSidebar(),
8922
9563
  }),