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
@@ -1,4 +1,4 @@
1
- import type { ClientContext, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
1
+ import type { ClientContext, SessionId, WorkspaceId } from './compat/types.ts'
2
2
  import { MobileNavToggle } from './MobileNavToggle.tsx'
3
3
  import { MobileNavOverlay } from './MobileNavOverlay.tsx'
4
4
  import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
@@ -17,6 +17,7 @@ import { installHeaderStatusDot } from './effects/header-status.ts'
17
17
  import { installGestures } from './effects/gestures.ts'
18
18
  import { installTurnFold } from './effects/turn-fold.ts'
19
19
  import { installModalBack } from './effects/modal-back.ts'
20
+ import { installModelSheetExtras } from './effects/model-sheet-extras.ts'
20
21
  import { installNativeTriggerOverlay } from './effects/native-trigger-overlay.ts'
21
22
  import { installWelcomeNoticeOptOut } from './effects/welcome-notice.ts'
22
23
  import { installKeyboardGuard } from './effects/keyboard-guard.ts'
@@ -34,6 +35,23 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
34
35
  /** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
35
36
  export const inject = ['slots', 'layout', 'locale', 'sessionLogDownload', 'sessions', 'workspaces']
36
37
 
38
+ /**
39
+ * 0.1.2 把新建会话和归档会话的界面级操作挪到了 uiWorkspace 服务;
40
+ * 0.1.1 没有这个服务,那两个操作还在 ctx.workspaces 上。这里按存在探测,
41
+ * 不把 uiWorkspace 写进 inject(写了 0.1.1 就永远不激活)。
42
+ * 类型用本地最小接口,不 import 那个 dsh-client-ui-workspace 包
43
+ * ——0.1.1 装不到。
44
+ */
45
+ interface UiWorkspaceLike {
46
+ startSession(workspaceId?: WorkspaceId): void
47
+ archiveSession(sessionId: SessionId): Promise<void>
48
+ }
49
+
50
+ /** 0.1.1 的 ctx.workspaces 上还有 startSession;0.1.2 的类型里没有了。 */
51
+ interface LegacyWorkspacesLike {
52
+ startSession(workspaceId?: WorkspaceId): void
53
+ }
54
+
37
55
  /**
38
56
  * Mobile-adaptive shell, browser half: injects the mobile stylesheet, then
39
57
  * contributes the directory toggle to the session header and the backdrop +
@@ -89,6 +107,9 @@ export function apply(ctx: ClientContext): void {
89
107
  installTurnFold(ctx)
90
108
  installModalBack(ctx)
91
109
  installNativeTriggerOverlay(ctx)
110
+ // Third-party composer entries (speed chip, vision toggle) move into the
111
+ // model sheet — the row has no width to spare and both are model settings.
112
+ installModelSheetExtras(ctx)
92
113
 
93
114
  // "内测声明" first-run notice: keep it visible (CSS-hiding it leaked the
94
115
  // dialog's #root inert lock — see effects/welcome-notice.ts) and offer a
@@ -163,7 +184,13 @@ export function apply(ctx: ClientContext): void {
163
184
  forkSession: (id: SessionId) => ctx.sessions.fork({ sessionId: id }),
164
185
  openSession: (id: SessionId) => ctx.sessions.open(id),
165
186
  renameSession: (id: SessionId, title: string) => ctx.sessions.binding(id)?.session.rename(title),
166
- archiveSession: (id: SessionId) => ctx.workspaces.archiveSession(id),
187
+ // 0.1.2 的界面级归档在 uiWorkspace(顺带清当前选中);懒查——回调是用户
188
+ // 点击才跑,那时服务必已注册。本插件的 inject 不包含 uiWorkspace(0.1.1
189
+ // 没有它),所以启动时不能查一次就用:可能早于它注册。
190
+ archiveSession: (id: SessionId) => {
191
+ const ui = ctx.get('uiWorkspace') as UiWorkspaceLike | undefined
192
+ return ui === undefined ? ctx.workspaces.archiveSession(id) : ui.archiveSession(id)
193
+ },
167
194
  downloadSessionLog: (id: SessionId) => ctx.sessionLogDownload.download(id),
168
195
  }),
169
196
  }, MobileSessionInfo))
@@ -215,11 +242,26 @@ export function apply(ctx: ClientContext): void {
215
242
  store: nav,
216
243
  inject: () => ({
217
244
  openSession: (id: SessionId) => ctx.sessions.open(id),
218
- startSession: (workspaceId?: WorkspaceId) => ctx.workspaces.startSession(workspaceId),
245
+ // uiWorkspace 懒查:回调是用户点击才跑,服务那时必已注册(inject 不含
246
+ // uiWorkspace,启动时可能先于它注册,不能启动查一次就用)。0.1.1 上恒
247
+ // undefined,退回 ctx.workspaces.startSession——0.1.1 专用分支,0.1.2 的
248
+ // 类型里该方法已不存在,故对类型做 LegacyWorkspacesLike cast,运行时走不到。
249
+ startSession: (workspaceId?: WorkspaceId) => {
250
+ const ui = ctx.get('uiWorkspace') as UiWorkspaceLike | undefined
251
+ return ui === undefined
252
+ ? (ctx.workspaces as unknown as LegacyWorkspacesLike).startSession(workspaceId)
253
+ : ui.startSession(workspaceId)
254
+ },
219
255
  // S5 session-log chip — the same service call MobileDrawerFooter uses.
220
256
  downloadSessionLog: (id: SessionId) => ctx.sessionLogDownload.download(id),
221
- // Row swipe action — the same binding MobileSessionInfo archives with.
222
- archiveSession: (id: SessionId) => ctx.workspaces.archiveSession(id),
257
+ // Row swipe action — the same binding MobileSessionInfo archives with;
258
+ // on 0.1.2 the uiWorkspace variant also clears the current selection
259
+ // when the archived session is the one selected. Same lazy lookup as
260
+ // the session-info sheet's archive binding above.
261
+ archiveSession: (id: SessionId) => {
262
+ const ui = ctx.get('uiWorkspace') as UiWorkspaceLike | undefined
263
+ return ui === undefined ? ctx.workspaces.archiveSession(id) : ui.archiveSession(id)
264
+ },
223
265
  }),
224
266
  }, MobileHome))
225
267
 
@@ -240,7 +282,10 @@ export function apply(ctx: ClientContext): void {
240
282
  order: 5,
241
283
  locale: NS,
242
284
  inject: () => ({
243
- downloadSessionLog: (sessionId: string) => ctx.sessionLogDownload.download(sessionId),
285
+ // The footer slot contract hands the id over as a loose string;
286
+ // download() wants the branded SessionId — pure type-layer narrowing
287
+ // (runtime value is a session id either way, same as MobileHome.tsx).
288
+ downloadSessionLog: (sessionId: string) => ctx.sessionLogDownload.download(sessionId as SessionId),
244
289
  toggleSidebar: () => ctx.layout.toggleSidebar(),
245
290
  }),
246
291
  }, MobileDrawerFooter))
@@ -254,3 +299,22 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
254
299
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
255
300
  import type {} from '@deepseek-ai/dsh-client-locale/client'
256
301
  import type {} from '@deepseek-ai/dsh-session-log-export/client'
302
+ // 0.1.2-only package: augments GlobalStandardProps with
303
+ // useSessions/useSessionPendingInteraction and SessionStandardProps with
304
+ // sessionId/useSession/useProjection, and declares the `ctx.uiSession`
305
+ // service. 0.1.1 has no such package — but this is `import type`, so the
306
+ // build output carries no trace of it and the runtime is unaffected.
307
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client'
308
+ // 0.1.2-only controller packages: their `/client` type entries merge
309
+ // `ctx.sessions` / `ctx.workspaces` onto the cordis Context (0.1.1 declared
310
+ // both from dsh-client-runtime). Type-only again — nothing to load at
311
+ // runtime, so a 0.1.1 installation is unaffected.
312
+ import type {} from '@deepseek-ai/dsh-api-session-controller/client'
313
+ import type {} from '@deepseek-ai/dsh-api-workspace-controller/client'
314
+ // 0.1.2-only, type-only: the renderer declares `ctx.slots` on the cordis
315
+ // Context (the service this plugin registers every slot through) and
316
+ // `dsh-subagent/client` types the subagent catalog rows the session header
317
+ // reads (SessionListState.subagentsByParent). 0.1.1 had both declared in the
318
+ // dsh-client-runtime bundle that no longer exists.
319
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
320
+ import type {} from '@deepseek-ai/dsh-subagent/client'
@@ -1,5 +1,5 @@
1
- import { defineStore } from '@deepseek-ai/dsh-client-runtime/client'
2
- import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
1
+ import { defineStore } from './compat/store.ts'
2
+ import type { WorkspaceId } from './compat/types.ts'
3
3
 
4
4
  /** The two levels of the phone page stack. */
5
5
  export type MobileView = 'home' | 'session'
@@ -1,4 +1,5 @@
1
- import type { SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
1
+ import { hasPendingInteraction } from './compat/types.ts'
2
+ import type { MobileSessionRow } from './compat/types.ts'
2
3
  import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
3
4
 
4
5
  /**
@@ -6,11 +7,19 @@ import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
6
7
  * Shared by the home-screen row dots (MobileHome.tsx) and the session
7
8
  * header's running indicator (effects/header-status.ts), which reads it
8
9
  * outside React — see that module for why.
9
- * @param row - session summary.
10
+ *
11
+ * Pending interaction has two version-specific sources: `pending` is the
12
+ * 0.1.2 source (the uiSession service / useSessionPendingInteraction map,
13
+ * which this dot's caller resolves), and `hasPendingInteraction(row)` is the
14
+ * 0.1.1 source (the row's own field). Each version hits exactly one of the
15
+ * two: on 0.1.2 the row field is gone so the row check is always false, and
16
+ * on 0.1.1 `pending` is always false because the caller has no uiSession.
17
+ * @param row - session row.
18
+ * @param pending - whether the uiSession pending map has this row (0.1.2).
10
19
  * @returns the dot state, or undefined when the row needs no dot.
11
20
  */
12
- export function dotState(row: SessionSummary): StateDotState | undefined {
13
- if (row.pendingInteraction !== undefined) return 'warning'
21
+ export function dotState(row: MobileSessionRow, pending?: boolean): StateDotState | undefined {
22
+ if (pending === true || hasPendingInteraction(row)) return 'warning'
14
23
  if (row.running) return 'ongoing'
15
24
  if (row.completed === true) return 'done'
16
25
  return undefined
@@ -877,22 +877,39 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
877
877
 
878
878
  /* ---------- dsh-vision-router: composer vision toggle (2026-08-26) ----------
879
879
  THIRD-PARTY COMPAT RULE — dsh-vision-router registers a Vision mode
880
- toggle into conversation.input.right (lib/client-presentation-boundary-
881
- main.js, slot id "vision-router-mode-toggle"): a bordered pill with
882
- THREE text spans (an eye emoji, the localized label, and a check mark
883
- while active) sized by INLINE styles — minHeight 28, padding 4px 8px,
884
- fontSize 12, white-space nowrap — roughly 90px of unshrinkable width.
880
+ toggle into conversation.input.right (slot id
881
+ "vision-router-mode-toggle"): a bordered pill sized by INLINE styles —
882
+ minHeight 28, padding 4px 8px, fontSize 12, white-space nowrap —
883
+ roughly 90px of unshrinkable width.
884
+
885
+ Its children changed shape in 2.1.x (re-measured on the real UI
886
+ 2026-09-04, plugin 2.1.1). Both forms are handled:
887
+ - text form (<= 2.0.x): <span>👁</span><span>label</span> plus a
888
+ <span>✓</span> while active;
889
+ - svg form (2.1.x, lib/client-presentation-boundary.js): a 14px eye
890
+ <svg>, <span>label</span>, plus a SECOND 12px check <svg> while
891
+ active.
892
+ The 2.1.1 update is what the user hit: the label span was still hidden
893
+ but the new eye svg was not, so it rendered NEXT TO our ::before eye —
894
+ two eyes in a 28px button — and the active check svg had nothing hiding
895
+ it at all, eating width the phone row does not have.
885
896
 
886
897
  The phone composer row is nowrap by design (composer.css.ts section 1)
887
898
  and the model seat is its only shrinkable item, so that pill squeezes
888
899
  the model name out of the row (user report, 2026-08-26).
889
900
 
890
901
  Phone treatment: a 28x28 round icon button in the attach button's
891
- language (same size, same neutral fill). All three spans are hidden and
892
- ONE 16px eye is drawn by a CSS mask on ::before with currentColor — the
893
- same survives-React-re-render reasoning as the auto-approve shield
894
- below: the button re-renders on every state flip, so a DOM-injected
895
- icon would need an observer while generated content does not. The
902
+ language (same size, same neutral fill), showing exactly ONE 16px eye
903
+ and nothing else. Every span is hidden (label, and the text form's
904
+ emoji and check); of the svg form's icons only the first survives, the
905
+ trailing check svg is hidden by a sibling combinator so the count is
906
+ never assumed. Our own ::before eye is now a FALLBACK, gated on
907
+ :has(svg) so it draws only for the text form — the plugin ships its own
908
+ icon from 2.1.x on, and drawing over it is what produced the double
909
+ eye. Generated content rather than a DOM-injected icon, for the same
910
+ survives-React-re-render reason as the auto-approve shield below: the
911
+ button re-renders on every state flip, so an injected node would need
912
+ an observer while ::before does not. The
896
913
  accessible name is unaffected: the button ships aria-label + title with
897
914
  the full enable/disable copy, and aria-pressed keeps conveying the
898
915
  state; visually the plugin's own inline styles still flip border +
@@ -935,12 +952,27 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
935
952
  background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
936
953
  box-shadow: none !important;
937
954
  }
938
- /* Eye emoji + label + active check: all three spans out. */
955
+ /* Text form: emoji + label + active check are all spans — all out. */
939
956
  [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > span {
940
957
  display: none !important;
941
958
  }
959
+ /* Svg form (2.1.x): the first svg IS the eye, so keep it and match the
960
+ row's other icons at 16px. */
961
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > svg:first-of-type {
962
+ width: 16px !important;
963
+ height: 16px !important;
964
+ }
965
+ /* ...and drop every svg after it — today that is the active-state check
966
+ mark. A sibling combinator rather than :nth-child(2): the count is the
967
+ plugin's business, this only says "one icon, the first one". */
968
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle] > svg ~ svg {
969
+ display: none !important;
970
+ }
942
971
  /* The one svg: eye outline + iris as a currentColor mask — brand while
943
972
  active, dimmed with the button by the plugin's own disabled opacity. */
973
+ [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]:has(svg)::before {
974
+ content: none !important;
975
+ }
944
976
  [data-slot="conversation.composer.bar"] [data-vision-router-mode-toggle]::before {
945
977
  content: '';
946
978
  width: 16px;
@@ -65,12 +65,28 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
65
65
  }
66
66
  ${ROW} > [class$="_tools"] > [class$="_modes"] {
67
67
  order: 3 !important;
68
- flex: 0 1 auto !important;
69
- min-width: 0 !important;
68
+ /* Rigid on purpose. This seat holds the permission chip, whose own
69
+ wrapper (the plugin's element inside _modes) is \`flex: 0 0 auto\` and
70
+ will NOT follow the seat down — so a shrinkable seat does not make the
71
+ chip narrower, it just slices the seat out from under a chip that
72
+ stays 44px wide, and the overflow lands on top of the model pill.
73
+ Measured 2026-09-04 on DSH 0.1.2 at 390px: seat 90-120 (29.7px) with
74
+ the chip still 90-134, model starting at 125 — a 9px overlap, exactly
75
+ the "the two chips touch" report. With the seat rigid: seat 90-134,
76
+ model 139-246, overlap 0; and at 320px the model pill shrinks to 52px
77
+ and still nothing overlaps.
78
+ This also restores what section 1 says out loud — the model pill is
79
+ the ONLY shrinkable item in the row. \`min-width: 0\` is gone with the
80
+ shrink: it only ever mattered as the companion of \`flex: 0 1\`. */
81
+ flex: 0 0 auto !important;
70
82
  gap: 6px !important;
71
83
  }
72
- /* The model seat carries the elastic gap: everything after it is pushed to
73
- the right edge, so no spacer element is needed. */
84
+ /* The elastic gap used to live here as \`margin-right: auto\`, which broke
85
+ the moment a session had no model pill: a subagent session has none, so
86
+ the gap vanished and the stop / send buttons bunched up in the middle of
87
+ the row instead of sitting at the right edge (reported 2026-09-06). The
88
+ gap belongs to the RIGHT group, not to the last item of the left one —
89
+ see the two rules below the order list. */
74
90
  ${MODEL} {
75
91
  order: 4 !important;
76
92
  /* A definite flex-basis (not auto) breaks the circular sizing between
@@ -81,7 +97,6 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
81
97
  flex: 0 1 min(48vw, 200px) !important;
82
98
  min-width: 0 !important;
83
99
  max-width: min(48vw, 200px) !important;
84
- margin-right: auto !important;
85
100
  }
86
101
  /* Third-party input.right entries park next to the ring rather than
87
102
  landing at order 0 (= far left) once the groups are flattened. */
@@ -96,6 +111,22 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
96
111
  ${ROW} > [class$="_trailing"] > [class$="_primary"] {
97
112
  order: 7 !important;
98
113
  }
114
+ /* The elastic gap. It sits in front of whichever of the two right-hand
115
+ items comes first, so the group hugs the right edge no matter what the
116
+ left side happens to contain — with a model pill, without one (subagent
117
+ sessions), with or without the third-party entries at order 5.
118
+ Both get \`margin-left: auto\` and the last rule takes it back off the
119
+ send button whenever a ring precedes it: two auto margins would SHARE
120
+ the free space and open a second gap between the ring and send. A
121
+ sibling combinator answers "is there a ring before me" without assuming
122
+ anything about the host's nesting. */
123
+ ${ROW} > [class$="_trailing"] > span[class$="_root"],
124
+ ${ROW} > [class$="_trailing"] > [class$="_primary"] {
125
+ margin-left: auto !important;
126
+ }
127
+ ${ROW} > [class$="_trailing"] > span[class$="_root"] ~ [class$="_primary"] {
128
+ margin-left: 0 !important;
129
+ }
99
130
 
100
131
  /* --- 3. permission as icon-only pill; model as name+effort pill ---
101
132
  (real-device round 2, 2026-08-17; model pill reworked 2026-08-25:
@@ -216,6 +247,115 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
216
247
  border-radius: 12px !important;
217
248
  font-size: 15px !important;
218
249
  }
250
+ /* --- 4a. third-party composer entries live in the model sheet ---
251
+ effects/model-sheet-extras.ts parks the \`conversation.input.right\`
252
+ container inside the model menu while that menu is open and puts it back
253
+ when it closes; these rules are the two halves of that trip.
254
+
255
+ In the row: hidden, but ONLY once the effect has marked the container as
256
+ one it manages. Keying the hide off the marker rather than off "not
257
+ currently parked" is what makes the failure direction safe — if the
258
+ effect declines to act (nothing in the slot but the vision toggle, so
259
+ nothing worth moving) or never runs at all, the marker is absent, this
260
+ rule does not match, and the controls stay exactly where the host put
261
+ them. The inverse spelling hid them in the row whenever they were not in
262
+ the sheet, which in those same cases left them reachable from neither
263
+ place (caught in review, 2026-09-06).
264
+ Hiding by ancestor rather than by a display rule on the container itself
265
+ is what lets the parked copy style itself freely — same element, two
266
+ homes, two rule sets. \`!important\` because the slot container carries an
267
+ inline \`display: contents\`. */
268
+ ${ROW} > [class$="_trailing"] > [data-slot="conversation.input.right"][data-zen-sheet-extras] {
269
+ display: none !important;
270
+ }
271
+ /* Parked in the sheet: a column of full-width rows in the sheet's own
272
+ language, so a relocated control reads as a sibling of 模型 and 推理等级
273
+ rather than a chip that wandered in. */
274
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] {
275
+ display: flex !important;
276
+ flex-direction: column !important;
277
+ gap: 2px !important;
278
+ width: 100% !important;
279
+ }
280
+ /* Undo the row's compaction on every parked control: back to full width and
281
+ the 48px the sheet's own cells use. \`> * > *\` reaches both shapes without
282
+ naming either plugin — subscriptions wraps its trigger in a positioning
283
+ div, vision-router puts its button straight in the slot. */
284
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > *,
285
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > * > button {
286
+ width: 100% !important;
287
+ max-width: none !important;
288
+ min-width: 0 !important;
289
+ justify-content: flex-start !important;
290
+ }
291
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] button {
292
+ box-sizing: border-box !important;
293
+ min-height: 48px !important;
294
+ height: auto !important;
295
+ padding: 0 12px !important;
296
+ gap: 10px !important;
297
+ border-radius: 12px !important;
298
+ border: none !important;
299
+ background: transparent !important;
300
+ font-size: 15px !important;
301
+ /* Both plugins center their chip label, which is right for a pill in the
302
+ row and wrong for a full-width row: the sheet's own cells start their
303
+ label at the left edge. justify-content covers the flex triggers,
304
+ text-align the ones that are not flex containers. */
305
+ text-align: left !important;
306
+ justify-content: flex-start !important;
307
+ }
308
+ /* Order comes from the row (compat.css.ts puts the vision toggle at 5 so it
309
+ parks beside the context ring), and that rule still matches in here — the
310
+ sheet is a DOM descendant of the composer bar even though it paints as a
311
+ fixed layer. Reset it so the sheet follows DOM order instead of
312
+ inheriting a decision that was about a different layout. */
313
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] > * {
314
+ order: 0 !important;
315
+ }
316
+ /* The vision toggle is squeezed to a 28px icon in the row (compat.css.ts);
317
+ in the sheet it is a row, so its label comes back and the icon leads. */
318
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > span {
319
+ display: inline !important;
320
+ }
321
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > svg:first-of-type {
322
+ width: 18px !important;
323
+ height: 18px !important;
324
+ }
325
+ /* The speed chip opens a menu of its own, anchored \`bottom: 100%\` to its
326
+ trigger. Inside the sheet — which is \`overflow: hidden\` so its rounded
327
+ top clips its content — that menu would be cut off. Same escape section 4
328
+ uses for the host's two menus: position: fixed leaves the clip behind
329
+ (the sheet sets \`transform: none\`, so nothing between here and the
330
+ viewport captures fixed positioning) and it lands as its own sheet on top
331
+ of this one. */
332
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [role="menu"] {
333
+ position: fixed !important;
334
+ left: 0 !important;
335
+ right: 0 !important;
336
+ bottom: 0 !important;
337
+ top: auto !important;
338
+ margin: 0 !important;
339
+ width: 100% !important;
340
+ min-width: 0 !important;
341
+ max-width: none !important;
342
+ max-height: min(70dvh, 520px) !important;
343
+ box-sizing: border-box !important;
344
+ border-radius: 16px 16px 0 0 !important;
345
+ border-bottom: none !important;
346
+ padding: 8px 8px calc(8px + var(--mnav-sab)) !important;
347
+ /* Above the model sheet it sits on (60, section 4), below the session-info
348
+ sheet's 70 (info.css.ts) — it is a child of the model sheet, not a peer
349
+ of the page's own layers. */
350
+ z-index: 61 !important;
351
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;
352
+ }
353
+ ${MODEL} > [class$="_menu"] > [data-zen-sheet-extras] [role="menu"] [role="menuitemradio"] {
354
+ min-height: 48px !important;
355
+ border-radius: 12px !important;
356
+ font-size: 15px !important;
357
+ }
358
+
219
359
  /* --- 4b. the official scroll-to-bottom button must not poke through the
220
360
  sheet (real-device follow-up, 2026-08-17) ---
221
361
  ChatView's own "jump to latest" button (aria-label t("chat.toBottom"))
@@ -250,6 +390,17 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
250
390
  [class$="_composerSeat"] {
251
391
  --dsh-composer-text-max-height: 124px;
252
392
  }
393
+ /* Pull the text in from the card's left border. Upstream insets the editing
394
+ host by 16px while the control row below it starts 8px in (row padding,
395
+ section 1), so the placeholder sat noticeably further out than the
396
+ buttons under it and the field read as over-padded on a phone (reported
397
+ 2026-09-06). 12px squares it with the 12px of clear space on the right,
398
+ giving the text a symmetric box, and closes most of the gap with the
399
+ button row. Anchored on the editing host's own contract attribute rather
400
+ than a hashed class. */
401
+ [data-slot="conversation.composer.bar"] [data-composer-input] {
402
+ padding-left: 12px !important;
403
+ }
253
404
 
254
405
  /* --- 6. no divider above OR below the message list ---
255
406
  Instead of a rule the messages butt against, they fade near both edges.
@@ -341,7 +492,13 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
341
492
  composer. Anchored on the official data-testid, a stable contract
342
493
  marker rather than a hashed class name. */
343
494
  [data-slot="conversation.input.dock"] > [data-testid="todo-panel"] {
344
- flex: 1 1 auto !important;
495
+ /* A whole line to itself. It shared line 0 with the pills until the goal
496
+ bar turned up (2026-09-06): two full-width things on one nowrap line
497
+ meant the to-do header shrank to a stub and the goal bar ran off the
498
+ right edge. Both are status strips you read at a glance, so they stack
499
+ — see the line policy in 7c. */
500
+ flex: 1 0 100% !important;
501
+ order: 1 !important;
345
502
  min-width: 0 !important;
346
503
  max-width: none !important;
347
504
  min-height: 0 !important;
@@ -368,7 +525,7 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
368
525
  display: flex !important;
369
526
  flex: 1 0 100% !important;
370
527
  flex-wrap: wrap !important;
371
- order: 2 !important;
528
+ order: 4 !important;
372
529
  align-items: center !important;
373
530
  gap: 6px !important;
374
531
  max-width: none !important;
@@ -403,12 +560,12 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
403
560
  sits closest to the input it will be sent from. The rail only wraps
404
561
  while one of the two full-width entries exists, so a session with
405
562
  neither keeps the original single scrolling chip line. */
406
- [data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"]) {
563
+ [data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"], > [data-testid="todo-panel"], > [data-goal-bar]) {
407
564
  flex-wrap: wrap !important;
408
565
  }
409
566
  [data-slot="conversation.input.dock"] > [data-queue-dock] {
410
567
  flex: 1 0 100% !important;
411
- order: 1 !important;
568
+ order: 3 !important;
412
569
  max-width: none !important;
413
570
  min-height: 0 !important;
414
571
  max-height: none !important;
@@ -419,6 +576,24 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
419
576
  font-size: 13px !important;
420
577
  line-height: normal !important;
421
578
  }
579
+ /* dsh-client-ui-goal's status strip (its own \`data-goal-bar\` marker; it
580
+ registers into this dock at order 10). Fourth thing here that is not a
581
+ pill: a row carrying the objective text plus pause / edit / delete
582
+ buttons, so the 26px cage clipped its buttons off the right edge and the
583
+ nowrap line pushed what was left off screen (reported 2026-09-06). Same
584
+ treatment the queue gets — a line of its own, official geometry back. */
585
+ [data-slot="conversation.input.dock"] > [data-goal-bar] {
586
+ flex: 1 0 100% !important;
587
+ order: 2 !important;
588
+ max-width: none !important;
589
+ min-height: 0 !important;
590
+ max-height: none !important;
591
+ border-radius: 12px !important;
592
+ overflow: visible !important;
593
+ font-size: 13px !important;
594
+ line-height: normal !important;
595
+ }
596
+
422
597
  /* The panel is the dock's only child; upstream rounds its top corners only
423
598
  and drops the bottom border, because there it is half-hidden behind the
424
599
  card. On its own line it is a free-standing strip, so close it up. */
@@ -34,6 +34,27 @@ export const INFO_CSS = `/* ---------- session-info sheet (< 768px) ---------- *
34
34
  display: block !important;
35
35
  }
36
36
 
37
+ /* The sheet renders inside the header, and \`[data-phase] header\` is a
38
+ stacking context of its own (position: relative + z-index: 2, set in
39
+ header.css.ts so the header's fade strip paints over the scroller). A
40
+ child's z-index is scoped to that context, so the z:70 below competes not
41
+ with the page but with the header's own 2 — and the host's composer seat
42
+ (.wSkVaW_composerSeat, z:7, from dsh-client-ui-conversation) outranks
43
+ it. Measured live on DSH 0.1.2, 2026-09-04: with the sheet open, the
44
+ topmost element over the composer band was the composer's own trigger
45
+ button, i.e. the input row painted THROUGH the card.
46
+ Promote the header itself for exactly as long as the sheet is mounted —
47
+ MobileSessionInfo returns null when closed (\`if (!open) return null\`),
48
+ so the header drops back to 2 on close (verified: 70 while open, 2
49
+ after). Descendant :has(), never \`>\`: the utilities slot wrapper is
50
+ \`display: contents\` and the host owns the depth (AGENTS.md — do not
51
+ encode host DOM depth in a selector).
52
+ Not raising the seat's own z-index instead: it belongs to the host and
53
+ is shared with the scroll-to-bottom button and the composer sheets. */
54
+ [data-phase] header:has([data-mobile-nav="info-layer"]) {
55
+ z-index: 70;
56
+ }
57
+
37
58
  [data-mobile-nav="info-layer"] {
38
59
  position: fixed;
39
60
  inset: 0;