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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/interface.md +6 -4
- package/docs/remote-access.md +1 -0
- package/lib/client.js +405 -276
- package/lib/types/client/MobileAttachButton.d.ts +15 -7
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -1
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -1
- package/lib/types/client/effects/model-sheet-extras.d.ts +29 -25
- package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/check-attach-upload.mjs +42 -0
- package/src/client/MobileAttachButton.tsx +23 -12
- package/src/client/MobileAttachChips.tsx +0 -7
- package/src/client/effects/model-sheet-extras.ts +233 -111
- package/src/client/styles/composer.css.ts +142 -83
- package/lib/types/client/host-attach.d.ts +0 -38
- package/lib/types/client/host-attach.d.ts.map +0 -1
- package/src/client/host-attach.ts +0 -54
|
@@ -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 >
|
|
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).
|
|
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
|
-
/**
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
340
|
-
effects/model-sheet-extras.ts
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
/*
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
/*
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
407
|
-
in the
|
|
408
|
-
|
|
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
|
-
|
|
412
|
-
width:
|
|
413
|
-
height:
|
|
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
|
|
417
|
-
|
|
418
|
-
uses for the host's two menus: position: fixed leaves the clip
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
}
|