dsh-zen-remote 2.2.7 → 2.2.8

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.
@@ -6,11 +6,16 @@
6
6
  //
7
7
  // Official structure this file reshapes (verified live at 390px, see the S3
8
8
  // report): InputBar renders
9
- // _card > _row > [_tools > (_add, _modes > PermissionSelect, input.left)]
9
+ // _card > _row > [_tools > (_add, input[type=file] hidden, _modes >
10
+ // PermissionSelect, input.left)]
10
11
  // [_trailing > (_standardControls > (input.right, input.model), _activity, _primary)]
11
12
  // with `_tools` / `_trailing` / `_standardControls` as inner flex groups
12
13
  // (0.1.7 added `_standardControls` and moved the ContextMeter out of the row
13
- // into the dock under the card). Flattening all three to
14
+ // into the dock under the card). That hidden file input is the stand-down
15
+ // probe host-attach.ts USED to key on — since #31 the phone's own attach
16
+ // button is a permanent resident instead, and DSH 0.1.5's second `_add`
17
+ // button (the standalone paperclip 0.2.x removed) is hidden by section 2.
18
+ // Flattening all three to
14
19
  // `display: contents` turns every control into a direct flex item of `_row`,
15
20
  // which is what makes a pure `order` reflow possible without touching a
16
21
  // single official component.
@@ -23,8 +28,21 @@ const ROOT = '[data-slot="conversation.composer.bar"] > [class$="_root"]'
23
28
  const RING_W = '30px'
24
29
  /** DSH 0.1.7's wrapper around input.right + input.model inside `_trailing`. */
25
30
  const STD = `${ROW} > [class$="_trailing"] > [class$="_standardControls"]`
26
- /** The model seat's own root inside the trailing group (long form so it beats the <=1023px pill rules in layout.css.ts). */
27
- const MODEL = `${STD} > [data-slot="conversation.input.model"] > [class$="_root"]`
31
+ /**
32
+ * The model seat's content, whichever plugin owns it: officially ModelSelect's
33
+ * `_root`, but a third party can replace the whole `conversation.input.model`
34
+ * slot (…codex-ui mounts its own selector there), and a subagent session has
35
+ * nothing matching `_root` at all. Spelled `*:not(…)` rather than bare `> *`
36
+ * on purpose: `:not([data-mobile-nav])` carries the same (0,1,0) the old
37
+ * `[class$="_root"]` did, keeping every rule below a full attribute CLASS
38
+ * ahead of the ≤1023px pill rules in layout.css.ts — their `[data-phase] …
39
+ * :has(…)` forms measure (0,6,1) on the trigger and (0,7,1) on the label
40
+ * with `!important`, so a bare `> *` would leave the phone winning by
41
+ * exactly one class on every MODEL-derived rule, one upstream attr away
42
+ * from losing. The :not() also keeps this plugin's own nodes from ever
43
+ * reading as the model seat, should one ever be mounted there.
44
+ */
45
+ const MODEL = `${STD} > [data-slot="conversation.input.model"] > *:not([data-mobile-nav])`
28
46
  /** The permission select's trigger button (structure only — the PermissionSelect hashed classes are never named). */
29
47
  const PERM = `${ROW} > [class$="_tools"] > [class$="_modes"]`
30
48
  /**
@@ -60,6 +78,12 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
60
78
  [data-mobile-nav="attach-picker"] {
61
79
  display: none !important;
62
80
  }
81
+ /* The composer-chips strip (effects/model-sheet-extras.ts) — same
82
+ every-width default as the picker above: the phone block re-shows it
83
+ inside the dock rail; the desktop shell keeps none. */
84
+ [data-mobile-nav="composer-chips"] {
85
+ display: none !important;
86
+ }
63
87
 
64
88
  @media (max-width: 767px) {
65
89
  /* --- 1. flatten the two official groups ---
@@ -83,8 +107,12 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
83
107
  }
84
108
 
85
109
  /* --- 2. the running order ---
86
- The attachment button (S3 placeholder, S7 wires it up) sits leftmost;
87
- the official "+" command menu keeps its seat right next to it, where
110
+ The attachment button (S3 placeholder, S7 wires it up) sits leftmost and
111
+ is a PERMANENT resident now (#31): on DSH 0.2.x the standalone paperclip
112
+ moved into the "+" command menu while its hidden <input type="file">
113
+ stayed in _tools, so the old host-attach stand-down probe matched on
114
+ every host and the phone composer was left with no attach affordance at
115
+ all. The official "+" command menu keeps its seat right next to it, where
88
116
  the two "insert something" affordances read as one cluster. */
89
117
  ${ROW} [data-mobile-nav="attach"] {
90
118
  order: 1 !important;
@@ -92,6 +120,14 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
92
120
  ${ROW} > [class$="_tools"] > [class$="_add"] {
93
121
  order: 2 !important;
94
122
  }
123
+ /* DSH ≤ 0.1.5 still renders that second _add button (the standalone
124
+ paperclip) as a direct _tools child; hide it below 768px so the row
125
+ keeps exactly one attach control — ours, which uploads into the session
126
+ workspace the phone can actually reach. On 0.2.x the only button in
127
+ _tools is the first of its type, so this never matches. */
128
+ ${ROW} > [class$="_tools"] > button[class$="_add"]:nth-of-type(2) {
129
+ display: none !important;
130
+ }
95
131
  ${ROW} > [class$="_tools"] > [class$="_modes"] {
96
132
  order: 3 !important;
97
133
  /* Rigid on purpose. This seat holds the permission chip, whose own
@@ -243,7 +279,8 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
243
279
  caps the pill so the row's elastic gap survives extreme model ids;
244
280
  the label rules below override the <=1023px grow rules in
245
281
  layout.css.ts (flex 1 1 auto) that would otherwise pad the pill. */
246
- ${MODEL} > [class$="_trigger"] {
282
+ ${MODEL} > [class$="_trigger"],
283
+ ${MODEL} > button {
247
284
  background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
248
285
  height: 30px !important;
249
286
  width: auto !important;
@@ -269,6 +306,18 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
269
306
  flex: none !important;
270
307
  white-space: nowrap !important;
271
308
  }
309
+ /* Third-party model selectors (…codex-ui) render a bare <button> with no
310
+ _trigger classes and text straight inside it; give that button the same
311
+ shrink+truncate contract the official label gets, so a long model id
312
+ ellipsizes instead of stretching the row. The official trigger also
313
+ matches this (harmless: it already ellipsizes its own label). */
314
+ ${MODEL} > button {
315
+ max-width: 100% !important;
316
+ min-width: 0 !important;
317
+ overflow: hidden !important;
318
+ text-overflow: ellipsis !important;
319
+ white-space: nowrap !important;
320
+ }
272
321
  /* PermissionSelect wraps its trigger in the Menu primitive's root span,
273
322
  which must shrink to the icon button's fixed width. */
274
323
  ${PERM} > span:has(> button[class$="_trigger"]) {
@@ -336,90 +385,97 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
336
385
  border-radius: 12px !important;
337
386
  font-size: 15px !important;
338
387
  }
339
- /* --- 4a. third-party composer entries live in the model sheet ---
340
- effects/model-sheet-extras.ts parks the \`conversation.input.right\`
341
- container inside the model menu while that menu is open and puts it back
342
- when it closes; these rules are the two halves of that trip.
388
+ /* --- 4a. third-party composer entries live in the dock's chip strip ---
389
+ effects/model-sheet-extras.ts collects the composer's secondary controls
390
+ (input.right, input.left, and anything a third party parks directly into
391
+ _tools / _standardControls / _trailing / _row) and moves the REAL nodes
392
+ into [data-mobile-nav="composer-chips"] — a 26px horizontally
393
+ scrollable strip the effect mounts at the head of the dock rail above
394
+ the card. Always visible, one tap, no sheet in between.
343
395
 
344
- In the row: hidden, but ONLY once the effect has marked the container as
345
- one it manages. Keying the hide off the marker rather than off "not
346
- currently parked" is what makes the failure direction safe — if the
347
- effect declines to act (nothing in the slot but the vision toggle, so
348
- nothing worth moving) or never runs at all, the marker is absent, this
349
- rule does not match, and the controls stay exactly where the host put
350
- them. The inverse spelling hid them in the row whenever they were not in
351
- the sheet, which in those same cases left them reachable from neither
352
- place (caught in review, 2026-09-06).
353
- Hiding by ancestor rather than by a display rule on the container itself
354
- is what lets the parked copy style itself freely — same element, two
355
- homes, two rule sets. \`!important\` because the slot container carries an
356
- inline \`display: contents\`. */
357
- ${STD} > [data-slot="conversation.input.right"][data-zen-sheet-extras] {
396
+ In the row: hidden, but ONLY once the effect has marked them. Two opt-in
397
+ markers, both written by the effect: the input.right CONTAINER gets
398
+ data-zen-sheet-extras (so an emptied wrapper leaves no flex gap), and
399
+ every collected node gets data-zen-composer-extra. Visibility comes back
400
+ the moment a node is parked (="open") or the effect releases it (marker
401
+ removed) — a version of the effect that declines to act, or never runs
402
+ at all, leaves every control exactly where the host put it; nothing can
403
+ end up hidden from both places at once (the 2026-09-06 review's rule). */
404
+ [data-slot="conversation.input.right"][data-zen-sheet-extras],
405
+ [data-zen-composer-extra]:not([data-zen-composer-extra="open"]) {
358
406
  display: none !important;
359
407
  }
360
- /* Parked in the sheet: a column of full-width rows in the sheet's own
361
- language, so a relocated control reads as a sibling of 模型 and 推理等级
362
- rather than a chip that wandered in. */
363
- ${MODEL_MENU} > [data-zen-sheet-extras] {
408
+ /* The strip itself: one 26px line at the rail's head (order 0, document
409
+ order first via the effect's prepend), scrolling sideways when the
410
+ collected controls overflow. Scoped to the dock CHILD — the generic
411
+ marker also sits on the emptied input.right container in the row, where
412
+ nothing but the opt-in hide above may touch it. This specificity (0,2,0)
413
+ is what beats the section 7 \`> *\` cage wherever they overlap (overflow,
414
+ max-width, radius are all !important there too). */
415
+ [data-slot="conversation.input.dock"] > [data-mobile-nav="composer-chips"] {
364
416
  display: flex !important;
365
- flex-direction: column !important;
366
- gap: 2px !important;
367
- width: 100% !important;
368
- }
369
- /* Undo the row's compaction on every parked control: back to full width and
370
- the 48px the sheet's own cells use. \`> * > *\` reaches both shapes without
371
- naming either plugin — subscriptions wraps its trigger in a positioning
372
- div, vision-router puts its button straight in the slot. */
373
- ${MODEL_MENU} > [data-zen-sheet-extras] > *,
374
- ${MODEL_MENU} > [data-zen-sheet-extras] > * > button {
375
- width: 100% !important;
376
- max-width: none !important;
417
+ flex-flow: row nowrap !important;
418
+ align-items: center !important;
419
+ gap: 6px !important;
420
+ order: 0 !important;
421
+ flex: 0 1 auto !important;
377
422
  min-width: 0 !important;
378
- justify-content: flex-start !important;
423
+ max-width: 100% !important;
424
+ min-height: 26px !important;
425
+ max-height: 26px !important;
426
+ border-radius: 0 !important;
427
+ overflow-x: auto !important;
428
+ overflow-y: hidden !important;
429
+ scrollbar-width: none !important;
379
430
  }
380
- ${MODEL_MENU} > [data-zen-sheet-extras] button {
381
- box-sizing: border-box !important;
382
- min-height: 48px !important;
383
- height: auto !important;
384
- padding: 0 12px !important;
385
- gap: 10px !important;
386
- border-radius: 12px !important;
387
- border: none !important;
388
- background: transparent !important;
389
- font-size: 15px !important;
390
- /* Both plugins center their chip label, which is right for a pill in the
391
- row and wrong for a full-width row: the sheet's own cells start their
392
- label at the left edge. justify-content covers the flex triggers,
393
- text-align the ones that are not flex containers. */
394
- text-align: left !important;
395
- justify-content: flex-start !important;
431
+ [data-slot="conversation.input.dock"] > [data-mobile-nav="composer-chips"]::-webkit-scrollbar {
432
+ display: none !important;
396
433
  }
397
- /* Order comes from the row (compat.css.ts puts the vision toggle at 5 so it
398
- parks beside the context ring), and that rule still matches in here on
399
- ≤ 0.1.2 — the inline sheet is a DOM descendant of the composer bar even
400
- though it paints as a fixed layer (the 0.1.5 body portal is not, where
401
- this is a harmless no-op). Reset it so the sheet follows DOM order
402
- instead of inheriting a decision that was about a different layout. */
403
- ${MODEL_MENU} > [data-zen-sheet-extras] > * {
434
+ /* Parked controls shrink from whatever the row allowed them to the rail's
435
+ 26px pill scale: auto width again (no full-width rows here), tight
436
+ height, and the chip font the rest of the rail uses. */
437
+ [data-mobile-nav="composer-chips"] > * {
438
+ flex: 0 0 auto !important;
404
439
  order: 0 !important;
440
+ display: inline-flex !important;
441
+ align-items: center !important;
442
+ min-height: 26px !important;
443
+ max-height: 26px !important;
444
+ width: auto !important;
445
+ max-width: none !important;
446
+ }
447
+ [data-mobile-nav="composer-chips"] button {
448
+ box-sizing: border-box !important;
449
+ height: 26px !important;
450
+ min-height: 26px !important;
451
+ max-height: 26px !important;
452
+ width: auto !important;
453
+ padding: 0 10px !important;
454
+ gap: 5px !important;
455
+ border-radius: 999px !important;
456
+ font-size: 11.5px !important;
457
+ line-height: 18px !important;
458
+ white-space: nowrap !important;
405
459
  }
406
- /* The vision toggle is squeezed to a 28px icon in the row (compat.css.ts);
407
- in the sheet it is a row, so its label comes back and the icon leads. */
408
- ${MODEL_MENU} > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > span {
460
+ /* The vision toggle is squeezed to a 28px icon-only button in the row
461
+ (compat.css.ts); in the strip it is a 14px icon + text pill, so its
462
+ label comes back. */
463
+ [data-mobile-nav="composer-chips"] [data-vision-router-mode-toggle] > span {
409
464
  display: inline !important;
410
465
  }
411
- ${MODEL_MENU} > [data-zen-sheet-extras] [data-vision-router-mode-toggle] > svg:first-of-type {
412
- width: 18px !important;
413
- height: 18px !important;
466
+ [data-mobile-nav="composer-chips"] [data-vision-router-mode-toggle] > svg:first-of-type {
467
+ width: 14px !important;
468
+ height: 14px !important;
414
469
  }
415
470
  /* The speed chip opens a menu of its own, anchored \`bottom: 100%\` to its
416
- trigger. Inside the sheet — which is \`overflow: hidden\` so its rounded
417
- top clips its content — that menu would be cut off. Same escape section 4
418
- uses for the host's two menus: position: fixed leaves the clip behind
419
- (the sheet sets \`transform: none\`, so nothing between here and the
420
- viewport captures fixed positioning) and it lands as its own sheet on top
421
- of this one. */
422
- ${MODEL_MENU} > [data-zen-sheet-extras] [role="menu"] {
471
+ trigger. Inside the strip — which is \`overflow-x: auto\`, so its content
472
+ clips at the strip's box — that menu would be cut off. Same escape
473
+ section 4 uses for the host's two menus: position: fixed leaves the clip
474
+ behind and it lands as its own bottom sheet over the composer. */
475
+ [class$="_composerSeat"]:has([data-zen-sheet-extras] [role="menu"]) {
476
+ z-index: 60 !important;
477
+ }
478
+ [data-zen-sheet-extras] [role="menu"] {
423
479
  position: fixed !important;
424
480
  left: 0 !important;
425
481
  right: 0 !important;
@@ -434,13 +490,13 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
434
490
  border-radius: 16px 16px 0 0 !important;
435
491
  border-bottom: none !important;
436
492
  padding: 8px 8px calc(8px + var(--mnav-sab)) !important;
437
- /* Above the model sheet it sits on (60, section 4), below the session-info
438
- sheet's 70 (info.css.ts) — it is a child of the model sheet, not a peer
439
- of the page's own layers. */
493
+ /* Above the page content behind the seat (the seat is raised to 60 by
494
+ the rule above while a parked menu is open), below the session-info
495
+ sheet's 70 (info.css.ts). */
440
496
  z-index: 61 !important;
441
497
  box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;
442
498
  }
443
- ${MODEL_MENU} > [data-zen-sheet-extras] [role="menu"] [role="menuitemradio"] {
499
+ [data-zen-sheet-extras] [role="menu"] [role="menuitemradio"] {
444
500
  min-height: 48px !important;
445
501
  border-radius: 12px !important;
446
502
  font-size: 15px !important;
@@ -468,7 +524,10 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
468
524
  body:has(${MODEL} > [class$="_menu"]) [data-chat-flow] + div,
469
525
  body:has(> [id$="-menu"][class$="_menu"]:is([role="menu"], [role="group"])) [data-chat-flow] + div,
470
526
  /* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */
471
- body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div {
527
+ body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div,
528
+ /* T-issue30: an inline menu a parked chip opened (escaped to a fixed
529
+ sheet by 4a). */
530
+ body:has([data-zen-sheet-extras] [role="menu"]) [data-chat-flow] + div {
472
531
  display: none !important;
473
532
  }
474
533
  /* --- 5. input box: two lines minimum, five lines maximum ---
@@ -1,38 +0,0 @@
1
- /**
2
- * The host composer's own hidden file picker, when the running DSH ships one.
3
- *
4
- * DSH 0.1.5 grew a native attachment flow (dsh-attachment / dsh-client-file-
5
- * upload): the official tool row carries a second `_add` paperclip whose click
6
- * drives an `<input type="file" hidden>` that is a DIRECT sibling of the
7
- * buttons inside `_tools`. That input is the one structural, locale-free
8
- * proof that the host implements composer attachment upload — the button
9
- * itself can only be told apart from the "+" command menu by its localized
10
- * aria-label, and the capability prop (`addFiles`) lives on the composer
11
- * bar's own inject face, which never reaches `conversation.input.left`
12
- * registrants. Scoped to `_tools` children so a third-party plugin parking
13
- * its own file input somewhere else in the composer cannot be misread as
14
- * the official feature; `:not([data-mobile-nav])` keeps OUR picker
15
- * (`attach-picker`) out of the match.
16
- */
17
- export declare const HOST_ATTACH_INPUT = "[data-slot=\"conversation.composer.bar\"] [class$=\"_tools\"] > input[type=\"file\"]:not([data-mobile-nav])";
18
- /** Synchronous read: does this host's composer ship its own attach picker? */
19
- export declare function hostAttachPresent(): boolean;
20
- /**
21
- * True while OUR attachment UI should mount (the host has none of its own).
22
- *
23
- * The rule this encodes (2026-09-11): where the host implements attachment
24
- * upload, the phone composer shows exactly ONE attach control — the host's —
25
- * and this plugin's S7 button and chips row stand down; on a host without
26
- * one (DSH ≤ 0.1.2), both load as they always have.
27
- *
28
- * `useLayoutEffect`, not `useEffect`: on the composer's very first mount our
29
- * slot child renders in the same React pass as the host's tool row, so the
30
- * state initializer below reads the DOM BEFORE either exists — the layout
31
- * effect re-checks after the commit and before the browser paints, so the
32
- * demotion to hidden (or, on an old host, the decision to stay) never
33
- * reaches the screen as a flash. The MutationObserver covers the lifetime
34
- * after that: the picker's existence only really changes with the page, but
35
- * a dynamic host plugin or a reload must not strand a stale verdict.
36
- */
37
- export declare function useOwnAttachUi(): boolean;
38
- //# sourceMappingURL=host-attach.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"host-attach.d.ts","sourceRoot":"","sources":["../../../src/client/host-attach.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,gHAC2E,CAAA;AAEzG,8EAA8E;AAC9E,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAUxC"}
@@ -1,54 +0,0 @@
1
- import { useLayoutEffect, useState } from 'react'
2
-
3
- /**
4
- * The host composer's own hidden file picker, when the running DSH ships one.
5
- *
6
- * DSH 0.1.5 grew a native attachment flow (dsh-attachment / dsh-client-file-
7
- * upload): the official tool row carries a second `_add` paperclip whose click
8
- * drives an `<input type="file" hidden>` that is a DIRECT sibling of the
9
- * buttons inside `_tools`. That input is the one structural, locale-free
10
- * proof that the host implements composer attachment upload — the button
11
- * itself can only be told apart from the "+" command menu by its localized
12
- * aria-label, and the capability prop (`addFiles`) lives on the composer
13
- * bar's own inject face, which never reaches `conversation.input.left`
14
- * registrants. Scoped to `_tools` children so a third-party plugin parking
15
- * its own file input somewhere else in the composer cannot be misread as
16
- * the official feature; `:not([data-mobile-nav])` keeps OUR picker
17
- * (`attach-picker`) out of the match.
18
- */
19
- export const HOST_ATTACH_INPUT =
20
- '[data-slot="conversation.composer.bar"] [class$="_tools"] > input[type="file"]:not([data-mobile-nav])'
21
-
22
- /** Synchronous read: does this host's composer ship its own attach picker? */
23
- export function hostAttachPresent(): boolean {
24
- return document.querySelector(HOST_ATTACH_INPUT) !== null
25
- }
26
-
27
- /**
28
- * True while OUR attachment UI should mount (the host has none of its own).
29
- *
30
- * The rule this encodes (2026-09-11): where the host implements attachment
31
- * upload, the phone composer shows exactly ONE attach control — the host's —
32
- * and this plugin's S7 button and chips row stand down; on a host without
33
- * one (DSH ≤ 0.1.2), both load as they always have.
34
- *
35
- * `useLayoutEffect`, not `useEffect`: on the composer's very first mount our
36
- * slot child renders in the same React pass as the host's tool row, so the
37
- * state initializer below reads the DOM BEFORE either exists — the layout
38
- * effect re-checks after the commit and before the browser paints, so the
39
- * demotion to hidden (or, on an old host, the decision to stay) never
40
- * reaches the screen as a flash. The MutationObserver covers the lifetime
41
- * after that: the picker's existence only really changes with the page, but
42
- * a dynamic host plugin or a reload must not strand a stale verdict.
43
- */
44
- export function useOwnAttachUi(): boolean {
45
- const [own, setOwn] = useState(() => !hostAttachPresent())
46
- useLayoutEffect(() => {
47
- const check = (): void => setOwn(!hostAttachPresent())
48
- const observer = new MutationObserver(check)
49
- observer.observe(document.body, { childList: true, subtree: true })
50
- check()
51
- return () => observer.disconnect()
52
- }, [])
53
- return own
54
- }