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
package/lib/client.js
CHANGED
|
@@ -7236,63 +7236,6 @@ function releaseThumbnailsExcept(keep) {
|
|
|
7236
7236
|
}
|
|
7237
7237
|
}
|
|
7238
7238
|
};
|
|
7239
|
-
__modules["client/host-attach.js"] = function (require, module, exports) {
|
|
7240
|
-
"use strict";
|
|
7241
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7242
|
-
exports.HOST_ATTACH_INPUT = void 0;
|
|
7243
|
-
exports.hostAttachPresent = hostAttachPresent;
|
|
7244
|
-
exports.useOwnAttachUi = useOwnAttachUi;
|
|
7245
|
-
const react_1 = require("react");
|
|
7246
|
-
/**
|
|
7247
|
-
* The host composer's own hidden file picker, when the running DSH ships one.
|
|
7248
|
-
*
|
|
7249
|
-
* DSH 0.1.5 grew a native attachment flow (dsh-attachment / dsh-client-file-
|
|
7250
|
-
* upload): the official tool row carries a second `_add` paperclip whose click
|
|
7251
|
-
* drives an `<input type="file" hidden>` that is a DIRECT sibling of the
|
|
7252
|
-
* buttons inside `_tools`. That input is the one structural, locale-free
|
|
7253
|
-
* proof that the host implements composer attachment upload — the button
|
|
7254
|
-
* itself can only be told apart from the "+" command menu by its localized
|
|
7255
|
-
* aria-label, and the capability prop (`addFiles`) lives on the composer
|
|
7256
|
-
* bar's own inject face, which never reaches `conversation.input.left`
|
|
7257
|
-
* registrants. Scoped to `_tools` children so a third-party plugin parking
|
|
7258
|
-
* its own file input somewhere else in the composer cannot be misread as
|
|
7259
|
-
* the official feature; `:not([data-mobile-nav])` keeps OUR picker
|
|
7260
|
-
* (`attach-picker`) out of the match.
|
|
7261
|
-
*/
|
|
7262
|
-
exports.HOST_ATTACH_INPUT = '[data-slot="conversation.composer.bar"] [class$="_tools"] > input[type="file"]:not([data-mobile-nav])';
|
|
7263
|
-
/** Synchronous read: does this host's composer ship its own attach picker? */
|
|
7264
|
-
function hostAttachPresent() {
|
|
7265
|
-
return document.querySelector(exports.HOST_ATTACH_INPUT) !== null;
|
|
7266
|
-
}
|
|
7267
|
-
/**
|
|
7268
|
-
* True while OUR attachment UI should mount (the host has none of its own).
|
|
7269
|
-
*
|
|
7270
|
-
* The rule this encodes (2026-09-11): where the host implements attachment
|
|
7271
|
-
* upload, the phone composer shows exactly ONE attach control — the host's —
|
|
7272
|
-
* and this plugin's S7 button and chips row stand down; on a host without
|
|
7273
|
-
* one (DSH ≤ 0.1.2), both load as they always have.
|
|
7274
|
-
*
|
|
7275
|
-
* `useLayoutEffect`, not `useEffect`: on the composer's very first mount our
|
|
7276
|
-
* slot child renders in the same React pass as the host's tool row, so the
|
|
7277
|
-
* state initializer below reads the DOM BEFORE either exists — the layout
|
|
7278
|
-
* effect re-checks after the commit and before the browser paints, so the
|
|
7279
|
-
* demotion to hidden (or, on an old host, the decision to stay) never
|
|
7280
|
-
* reaches the screen as a flash. The MutationObserver covers the lifetime
|
|
7281
|
-
* after that: the picker's existence only really changes with the page, but
|
|
7282
|
-
* a dynamic host plugin or a reload must not strand a stale verdict.
|
|
7283
|
-
*/
|
|
7284
|
-
function useOwnAttachUi() {
|
|
7285
|
-
const [own, setOwn] = (0, react_1.useState)(() => !hostAttachPresent());
|
|
7286
|
-
(0, react_1.useLayoutEffect)(() => {
|
|
7287
|
-
const check = () => setOwn(!hostAttachPresent());
|
|
7288
|
-
const observer = new MutationObserver(check);
|
|
7289
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
7290
|
-
check();
|
|
7291
|
-
return () => observer.disconnect();
|
|
7292
|
-
}, []);
|
|
7293
|
-
return own;
|
|
7294
|
-
}
|
|
7295
|
-
};
|
|
7296
7239
|
__modules["client/MobileAttachButton.js"] = function (require, module, exports) {
|
|
7297
7240
|
"use strict";
|
|
7298
7241
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -7301,7 +7244,6 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
7301
7244
|
const react_1 = require("react");
|
|
7302
7245
|
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
7303
7246
|
const attach_upload_ts_1 = require("./client/attach-upload.js");
|
|
7304
|
-
const host_attach_ts_1 = require("./client/host-attach.js");
|
|
7305
7247
|
/** Post one file to the host route and return the workspace-relative path it landed on. */
|
|
7306
7248
|
async function uploadFile(sessionId, file) {
|
|
7307
7249
|
const response = await fetch((0, attach_upload_ts_1.uploadUrl)(sessionId, file.name), {
|
|
@@ -7327,12 +7269,20 @@ async function uploadFile(sessionId, file) {
|
|
|
7327
7269
|
* the CLIENT's picker — no `accept` attribute on purpose, so iOS offers the
|
|
7328
7270
|
* full 相册 / 拍照 / 选取文件 sheet rather than one of them.
|
|
7329
7271
|
*
|
|
7330
|
-
* DSH 0.1.5 gave the official tool row its own paperclip
|
|
7331
|
-
*
|
|
7332
|
-
*
|
|
7333
|
-
*
|
|
7334
|
-
*
|
|
7335
|
-
*
|
|
7272
|
+
* DSH 0.1.5 gave the official tool row its own paperclip, and this button
|
|
7273
|
+
* used to stand down whenever that flow's hidden `<input type="file">` was
|
|
7274
|
+
* probed in the composer (that probe file is deleted with #31), keeping one
|
|
7275
|
+
* attach control per composer. DSH 0.2.x then removed the standalone
|
|
7276
|
+
* paperclip (the file action moved into the `+` command menu) while KEEPING
|
|
7277
|
+
* the hidden input, so the probe matched on every host and the phone composer
|
|
7278
|
+
* was left with no attach affordance at all. The phone shell now always
|
|
7279
|
+
* renders this button — its picker is the CLIENT's (相册 / 拍照 / 选取文件),
|
|
7280
|
+
* and every attachment takes the same `@.dsh-uploads/` draft-token path on
|
|
7281
|
+
* every DSH version. On DSH ≤ 0.1.5, which still ships the second official
|
|
7282
|
+
* paperclip, composer.css.ts hides it below 768px so the row keeps exactly
|
|
7283
|
+
* one attach control. Subagent sessions stay buttonless, mirroring the host:
|
|
7284
|
+
* the official InputBar disables its own file input while
|
|
7285
|
+
* `subagent !== null`.
|
|
7336
7286
|
*
|
|
7337
7287
|
* Every picked file takes the SAME path: upload to the node half, then append
|
|
7338
7288
|
* `@.dsh-uploads/name` to the draft through the official write path — no DOM
|
|
@@ -7347,21 +7297,25 @@ async function uploadFile(sessionId, file) {
|
|
|
7347
7297
|
* Files upload one at a time: a phone uplink gains nothing from parallelism
|
|
7348
7298
|
* and a serial loop keeps the mentions in pick order.
|
|
7349
7299
|
*/
|
|
7350
|
-
function MobileAttachButton({ t, sessionId, useInput, inputActions }) {
|
|
7300
|
+
function MobileAttachButton({ t, sessionId, useInput, inputActions, useSessions }) {
|
|
7351
7301
|
const picker = (0, react_1.useRef)(null);
|
|
7352
7302
|
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
7353
7303
|
const [error, setError] = (0, react_1.useState)(null);
|
|
7354
7304
|
const draft = useInput((state) => state.draft);
|
|
7355
|
-
|
|
7305
|
+
// Same session-row read the share icon/menu use (RemoteHeaderIcon.tsx):
|
|
7306
|
+
// `origin: 'subagent'` is the only subagent signal that reaches slot
|
|
7307
|
+
// registrants — the InputBar's own `subagent` prop does not.
|
|
7308
|
+
const row = useSessions((sessions) => sessions.byId[sessionId]);
|
|
7356
7309
|
// Uploads are awaited, so the `draft` captured at pick time goes stale — the
|
|
7357
7310
|
// user can keep typing while a file is in flight. The ref is refreshed on
|
|
7358
7311
|
// every render (useInput re-renders us on each draft change), so the loop
|
|
7359
7312
|
// below can rebase onto whatever the composer holds right now.
|
|
7360
7313
|
const liveDraft = (0, react_1.useRef)(draft);
|
|
7361
7314
|
liveDraft.current = draft;
|
|
7362
|
-
//
|
|
7363
|
-
//
|
|
7364
|
-
|
|
7315
|
+
// Subagent sessions get no attach affordance, mirroring the host (its own
|
|
7316
|
+
// file input is disabled while a subagent session is open). All hooks stay
|
|
7317
|
+
// above this line.
|
|
7318
|
+
if (row?.origin === 'subagent')
|
|
7365
7319
|
return null;
|
|
7366
7320
|
const pick = async (event) => {
|
|
7367
7321
|
const files = Array.from(event.target.files ?? []);
|
|
@@ -7410,7 +7364,6 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
7410
7364
|
const react_1 = require("react");
|
|
7411
7365
|
const dsh_client_ui_primitives_1 = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
7412
7366
|
const attach_upload_ts_1 = require("./client/attach-upload.js");
|
|
7413
|
-
const host_attach_ts_1 = require("./client/host-attach.js");
|
|
7414
7367
|
/**
|
|
7415
7368
|
* Attachment preview row (S7.1), in `conversation.input.dock` — the official
|
|
7416
7369
|
* full-width row above the composer card, which composer.css.ts already lays
|
|
@@ -7439,16 +7392,9 @@ const host_attach_ts_1 = require("./client/host-attach.js");
|
|
|
7439
7392
|
*/
|
|
7440
7393
|
function MobileAttachChips({ t, useInput, inputActions }) {
|
|
7441
7394
|
const draft = useInput((state) => state.draft);
|
|
7442
|
-
const ownUi = (0, host_attach_ts_1.useOwnAttachUi)();
|
|
7443
7395
|
(0, react_1.useEffect)(() => {
|
|
7444
7396
|
(0, attach_upload_ts_1.releaseThumbnailsExcept)((0, attach_upload_ts_1.mentionsIn)(draft));
|
|
7445
7397
|
}, [draft]);
|
|
7446
|
-
// Same gate as MobileAttachButton (host-attach.ts): where the host ships
|
|
7447
|
-
// its own attachment flow, our chips row stands down too — the official
|
|
7448
|
-
// rail owns the preview seat. The effect above still runs, so object URLs
|
|
7449
|
-
// from OUR flow get revoked even after the switch.
|
|
7450
|
-
if (!ownUi)
|
|
7451
|
-
return null;
|
|
7452
7398
|
const attachments = (0, attach_upload_ts_1.mentionsIn)(draft);
|
|
7453
7399
|
if (attachments.length === 0)
|
|
7454
7400
|
return null;
|
|
@@ -10613,11 +10559,16 @@ __modules["client/styles/composer.css.js"] = function (require, module, exports)
|
|
|
10613
10559
|
//
|
|
10614
10560
|
// Official structure this file reshapes (verified live at 390px, see the S3
|
|
10615
10561
|
// report): InputBar renders
|
|
10616
|
-
// _card > _row > [_tools > (_add, _modes >
|
|
10562
|
+
// _card > _row > [_tools > (_add, input[type=file] hidden, _modes >
|
|
10563
|
+
// PermissionSelect, input.left)]
|
|
10617
10564
|
// [_trailing > (_standardControls > (input.right, input.model), _activity, _primary)]
|
|
10618
10565
|
// with `_tools` / `_trailing` / `_standardControls` as inner flex groups
|
|
10619
10566
|
// (0.1.7 added `_standardControls` and moved the ContextMeter out of the row
|
|
10620
|
-
// into the dock under the card).
|
|
10567
|
+
// into the dock under the card). That hidden file input is the stand-down
|
|
10568
|
+
// probe host-attach.ts USED to key on — since #31 the phone's own attach
|
|
10569
|
+
// button is a permanent resident instead, and DSH 0.1.5's second `_add`
|
|
10570
|
+
// button (the standalone paperclip 0.2.x removed) is hidden by section 2.
|
|
10571
|
+
// Flattening all three to
|
|
10621
10572
|
// `display: contents` turns every control into a direct flex item of `_row`,
|
|
10622
10573
|
// which is what makes a pure `order` reflow possible without touching a
|
|
10623
10574
|
// single official component.
|
|
@@ -10631,8 +10582,21 @@ const ROOT = '[data-slot="conversation.composer.bar"] > [class$="_root"]';
|
|
|
10631
10582
|
const RING_W = '30px';
|
|
10632
10583
|
/** DSH 0.1.7's wrapper around input.right + input.model inside `_trailing`. */
|
|
10633
10584
|
const STD = `${ROW} > [class$="_trailing"] > [class$="_standardControls"]`;
|
|
10634
|
-
/**
|
|
10635
|
-
|
|
10585
|
+
/**
|
|
10586
|
+
* The model seat's content, whichever plugin owns it: officially ModelSelect's
|
|
10587
|
+
* `_root`, but a third party can replace the whole `conversation.input.model`
|
|
10588
|
+
* slot (…codex-ui mounts its own selector there), and a subagent session has
|
|
10589
|
+
* nothing matching `_root` at all. Spelled `*:not(…)` rather than bare `> *`
|
|
10590
|
+
* on purpose: `:not([data-mobile-nav])` carries the same (0,1,0) the old
|
|
10591
|
+
* `[class$="_root"]` did, keeping every rule below a full attribute CLASS
|
|
10592
|
+
* ahead of the ≤1023px pill rules in layout.css.ts — their `[data-phase] …
|
|
10593
|
+
* :has(…)` forms measure (0,6,1) on the trigger and (0,7,1) on the label
|
|
10594
|
+
* with `!important`, so a bare `> *` would leave the phone winning by
|
|
10595
|
+
* exactly one class on every MODEL-derived rule, one upstream attr away
|
|
10596
|
+
* from losing. The :not() also keeps this plugin's own nodes from ever
|
|
10597
|
+
* reading as the model seat, should one ever be mounted there.
|
|
10598
|
+
*/
|
|
10599
|
+
const MODEL = `${STD} > [data-slot="conversation.input.model"] > *:not([data-mobile-nav])`;
|
|
10636
10600
|
/** The permission select's trigger button (structure only — the PermissionSelect hashed classes are never named). */
|
|
10637
10601
|
const PERM = `${ROW} > [class$="_tools"] > [class$="_modes"]`;
|
|
10638
10602
|
/**
|
|
@@ -10667,6 +10631,12 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10667
10631
|
[data-mobile-nav="attach-picker"] {
|
|
10668
10632
|
display: none !important;
|
|
10669
10633
|
}
|
|
10634
|
+
/* The composer-chips strip (effects/model-sheet-extras.ts) — same
|
|
10635
|
+
every-width default as the picker above: the phone block re-shows it
|
|
10636
|
+
inside the dock rail; the desktop shell keeps none. */
|
|
10637
|
+
[data-mobile-nav="composer-chips"] {
|
|
10638
|
+
display: none !important;
|
|
10639
|
+
}
|
|
10670
10640
|
|
|
10671
10641
|
@media (max-width: 767px) {
|
|
10672
10642
|
/* --- 1. flatten the two official groups ---
|
|
@@ -10690,8 +10660,12 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10690
10660
|
}
|
|
10691
10661
|
|
|
10692
10662
|
/* --- 2. the running order ---
|
|
10693
|
-
The attachment button (S3 placeholder, S7 wires it up) sits leftmost
|
|
10694
|
-
|
|
10663
|
+
The attachment button (S3 placeholder, S7 wires it up) sits leftmost and
|
|
10664
|
+
is a PERMANENT resident now (#31): on DSH 0.2.x the standalone paperclip
|
|
10665
|
+
moved into the "+" command menu while its hidden <input type="file">
|
|
10666
|
+
stayed in _tools, so the old host-attach stand-down probe matched on
|
|
10667
|
+
every host and the phone composer was left with no attach affordance at
|
|
10668
|
+
all. The official "+" command menu keeps its seat right next to it, where
|
|
10695
10669
|
the two "insert something" affordances read as one cluster. */
|
|
10696
10670
|
${ROW} [data-mobile-nav="attach"] {
|
|
10697
10671
|
order: 1 !important;
|
|
@@ -10699,6 +10673,14 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10699
10673
|
${ROW} > [class$="_tools"] > [class$="_add"] {
|
|
10700
10674
|
order: 2 !important;
|
|
10701
10675
|
}
|
|
10676
|
+
/* DSH ≤ 0.1.5 still renders that second _add button (the standalone
|
|
10677
|
+
paperclip) as a direct _tools child; hide it below 768px so the row
|
|
10678
|
+
keeps exactly one attach control — ours, which uploads into the session
|
|
10679
|
+
workspace the phone can actually reach. On 0.2.x the only button in
|
|
10680
|
+
_tools is the first of its type, so this never matches. */
|
|
10681
|
+
${ROW} > [class$="_tools"] > button[class$="_add"]:nth-of-type(2) {
|
|
10682
|
+
display: none !important;
|
|
10683
|
+
}
|
|
10702
10684
|
${ROW} > [class$="_tools"] > [class$="_modes"] {
|
|
10703
10685
|
order: 3 !important;
|
|
10704
10686
|
/* Rigid on purpose. This seat holds the permission chip, whose own
|
|
@@ -10850,7 +10832,8 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10850
10832
|
caps the pill so the row's elastic gap survives extreme model ids;
|
|
10851
10833
|
the label rules below override the <=1023px grow rules in
|
|
10852
10834
|
layout.css.ts (flex 1 1 auto) that would otherwise pad the pill. */
|
|
10853
|
-
${MODEL} > [class$="_trigger"]
|
|
10835
|
+
${MODEL} > [class$="_trigger"],
|
|
10836
|
+
${MODEL} > button {
|
|
10854
10837
|
background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
|
|
10855
10838
|
height: 30px !important;
|
|
10856
10839
|
width: auto !important;
|
|
@@ -10876,6 +10859,18 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10876
10859
|
flex: none !important;
|
|
10877
10860
|
white-space: nowrap !important;
|
|
10878
10861
|
}
|
|
10862
|
+
/* Third-party model selectors (…codex-ui) render a bare <button> with no
|
|
10863
|
+
_trigger classes and text straight inside it; give that button the same
|
|
10864
|
+
shrink+truncate contract the official label gets, so a long model id
|
|
10865
|
+
ellipsizes instead of stretching the row. The official trigger also
|
|
10866
|
+
matches this (harmless: it already ellipsizes its own label). */
|
|
10867
|
+
${MODEL} > button {
|
|
10868
|
+
max-width: 100% !important;
|
|
10869
|
+
min-width: 0 !important;
|
|
10870
|
+
overflow: hidden !important;
|
|
10871
|
+
text-overflow: ellipsis !important;
|
|
10872
|
+
white-space: nowrap !important;
|
|
10873
|
+
}
|
|
10879
10874
|
/* PermissionSelect wraps its trigger in the Menu primitive's root span,
|
|
10880
10875
|
which must shrink to the icon button's fixed width. */
|
|
10881
10876
|
${PERM} > span:has(> button[class$="_trigger"]) {
|
|
@@ -10943,90 +10938,97 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
10943
10938
|
border-radius: 12px !important;
|
|
10944
10939
|
font-size: 15px !important;
|
|
10945
10940
|
}
|
|
10946
|
-
/* --- 4a. third-party composer entries live in the
|
|
10947
|
-
effects/model-sheet-extras.ts
|
|
10948
|
-
|
|
10949
|
-
|
|
10941
|
+
/* --- 4a. third-party composer entries live in the dock's chip strip ---
|
|
10942
|
+
effects/model-sheet-extras.ts collects the composer's secondary controls
|
|
10943
|
+
(input.right, input.left, and anything a third party parks directly into
|
|
10944
|
+
_tools / _standardControls / _trailing / _row) and moves the REAL nodes
|
|
10945
|
+
into [data-mobile-nav="composer-chips"] — a 26px horizontally
|
|
10946
|
+
scrollable strip the effect mounts at the head of the dock rail above
|
|
10947
|
+
the card. Always visible, one tap, no sheet in between.
|
|
10950
10948
|
|
|
10951
|
-
In the row: hidden, but ONLY once the effect has marked
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
is what lets the parked copy style itself freely — same element, two
|
|
10962
|
-
homes, two rule sets. \`!important\` because the slot container carries an
|
|
10963
|
-
inline \`display: contents\`. */
|
|
10964
|
-
${STD} > [data-slot="conversation.input.right"][data-zen-sheet-extras] {
|
|
10949
|
+
In the row: hidden, but ONLY once the effect has marked them. Two opt-in
|
|
10950
|
+
markers, both written by the effect: the input.right CONTAINER gets
|
|
10951
|
+
data-zen-sheet-extras (so an emptied wrapper leaves no flex gap), and
|
|
10952
|
+
every collected node gets data-zen-composer-extra. Visibility comes back
|
|
10953
|
+
the moment a node is parked (="open") or the effect releases it (marker
|
|
10954
|
+
removed) — a version of the effect that declines to act, or never runs
|
|
10955
|
+
at all, leaves every control exactly where the host put it; nothing can
|
|
10956
|
+
end up hidden from both places at once (the 2026-09-06 review's rule). */
|
|
10957
|
+
[data-slot="conversation.input.right"][data-zen-sheet-extras],
|
|
10958
|
+
[data-zen-composer-extra]:not([data-zen-composer-extra="open"]) {
|
|
10965
10959
|
display: none !important;
|
|
10966
10960
|
}
|
|
10967
|
-
/*
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10961
|
+
/* The strip itself: one 26px line at the rail's head (order 0, document
|
|
10962
|
+
order first via the effect's prepend), scrolling sideways when the
|
|
10963
|
+
collected controls overflow. Scoped to the dock CHILD — the generic
|
|
10964
|
+
marker also sits on the emptied input.right container in the row, where
|
|
10965
|
+
nothing but the opt-in hide above may touch it. This specificity (0,2,0)
|
|
10966
|
+
is what beats the section 7 \`> *\` cage wherever they overlap (overflow,
|
|
10967
|
+
max-width, radius are all !important there too). */
|
|
10968
|
+
[data-slot="conversation.input.dock"] > [data-mobile-nav="composer-chips"] {
|
|
10971
10969
|
display: flex !important;
|
|
10972
|
-
flex-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
the 48px the sheet's own cells use. \`> * > *\` reaches both shapes without
|
|
10978
|
-
naming either plugin — subscriptions wraps its trigger in a positioning
|
|
10979
|
-
div, vision-router puts its button straight in the slot. */
|
|
10980
|
-
${MODEL_MENU} > [data-zen-sheet-extras] > *,
|
|
10981
|
-
${MODEL_MENU} > [data-zen-sheet-extras] > * > button {
|
|
10982
|
-
width: 100% !important;
|
|
10983
|
-
max-width: none !important;
|
|
10970
|
+
flex-flow: row nowrap !important;
|
|
10971
|
+
align-items: center !important;
|
|
10972
|
+
gap: 6px !important;
|
|
10973
|
+
order: 0 !important;
|
|
10974
|
+
flex: 0 1 auto !important;
|
|
10984
10975
|
min-width: 0 !important;
|
|
10985
|
-
|
|
10976
|
+
max-width: 100% !important;
|
|
10977
|
+
min-height: 26px !important;
|
|
10978
|
+
max-height: 26px !important;
|
|
10979
|
+
border-radius: 0 !important;
|
|
10980
|
+
overflow-x: auto !important;
|
|
10981
|
+
overflow-y: hidden !important;
|
|
10982
|
+
scrollbar-width: none !important;
|
|
10986
10983
|
}
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
min-height: 48px !important;
|
|
10990
|
-
height: auto !important;
|
|
10991
|
-
padding: 0 12px !important;
|
|
10992
|
-
gap: 10px !important;
|
|
10993
|
-
border-radius: 12px !important;
|
|
10994
|
-
border: none !important;
|
|
10995
|
-
background: transparent !important;
|
|
10996
|
-
font-size: 15px !important;
|
|
10997
|
-
/* Both plugins center their chip label, which is right for a pill in the
|
|
10998
|
-
row and wrong for a full-width row: the sheet's own cells start their
|
|
10999
|
-
label at the left edge. justify-content covers the flex triggers,
|
|
11000
|
-
text-align the ones that are not flex containers. */
|
|
11001
|
-
text-align: left !important;
|
|
11002
|
-
justify-content: flex-start !important;
|
|
10984
|
+
[data-slot="conversation.input.dock"] > [data-mobile-nav="composer-chips"]::-webkit-scrollbar {
|
|
10985
|
+
display: none !important;
|
|
11003
10986
|
}
|
|
11004
|
-
/*
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
instead of inheriting a decision that was about a different layout. */
|
|
11010
|
-
${MODEL_MENU} > [data-zen-sheet-extras] > * {
|
|
10987
|
+
/* Parked controls shrink from whatever the row allowed them to the rail's
|
|
10988
|
+
26px pill scale: auto width again (no full-width rows here), tight
|
|
10989
|
+
height, and the chip font the rest of the rail uses. */
|
|
10990
|
+
[data-mobile-nav="composer-chips"] > * {
|
|
10991
|
+
flex: 0 0 auto !important;
|
|
11011
10992
|
order: 0 !important;
|
|
10993
|
+
display: inline-flex !important;
|
|
10994
|
+
align-items: center !important;
|
|
10995
|
+
min-height: 26px !important;
|
|
10996
|
+
max-height: 26px !important;
|
|
10997
|
+
width: auto !important;
|
|
10998
|
+
max-width: none !important;
|
|
10999
|
+
}
|
|
11000
|
+
[data-mobile-nav="composer-chips"] button {
|
|
11001
|
+
box-sizing: border-box !important;
|
|
11002
|
+
height: 26px !important;
|
|
11003
|
+
min-height: 26px !important;
|
|
11004
|
+
max-height: 26px !important;
|
|
11005
|
+
width: auto !important;
|
|
11006
|
+
padding: 0 10px !important;
|
|
11007
|
+
gap: 5px !important;
|
|
11008
|
+
border-radius: 999px !important;
|
|
11009
|
+
font-size: 11.5px !important;
|
|
11010
|
+
line-height: 18px !important;
|
|
11011
|
+
white-space: nowrap !important;
|
|
11012
11012
|
}
|
|
11013
|
-
/* The vision toggle is squeezed to a 28px icon in the row
|
|
11014
|
-
in the
|
|
11015
|
-
|
|
11013
|
+
/* The vision toggle is squeezed to a 28px icon-only button in the row
|
|
11014
|
+
(compat.css.ts); in the strip it is a 14px icon + text pill, so its
|
|
11015
|
+
label comes back. */
|
|
11016
|
+
[data-mobile-nav="composer-chips"] [data-vision-router-mode-toggle] > span {
|
|
11016
11017
|
display: inline !important;
|
|
11017
11018
|
}
|
|
11018
|
-
|
|
11019
|
-
width:
|
|
11020
|
-
height:
|
|
11019
|
+
[data-mobile-nav="composer-chips"] [data-vision-router-mode-toggle] > svg:first-of-type {
|
|
11020
|
+
width: 14px !important;
|
|
11021
|
+
height: 14px !important;
|
|
11021
11022
|
}
|
|
11022
11023
|
/* The speed chip opens a menu of its own, anchored \`bottom: 100%\` to its
|
|
11023
|
-
trigger. Inside the
|
|
11024
|
-
|
|
11025
|
-
uses for the host's two menus: position: fixed leaves the clip
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11024
|
+
trigger. Inside the strip — which is \`overflow-x: auto\`, so its content
|
|
11025
|
+
clips at the strip's box — that menu would be cut off. Same escape
|
|
11026
|
+
section 4 uses for the host's two menus: position: fixed leaves the clip
|
|
11027
|
+
behind and it lands as its own bottom sheet over the composer. */
|
|
11028
|
+
[class$="_composerSeat"]:has([data-zen-sheet-extras] [role="menu"]) {
|
|
11029
|
+
z-index: 60 !important;
|
|
11030
|
+
}
|
|
11031
|
+
[data-zen-sheet-extras] [role="menu"] {
|
|
11030
11032
|
position: fixed !important;
|
|
11031
11033
|
left: 0 !important;
|
|
11032
11034
|
right: 0 !important;
|
|
@@ -11041,13 +11043,13 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11041
11043
|
border-radius: 16px 16px 0 0 !important;
|
|
11042
11044
|
border-bottom: none !important;
|
|
11043
11045
|
padding: 8px 8px calc(8px + var(--mnav-sab)) !important;
|
|
11044
|
-
/* Above the
|
|
11045
|
-
|
|
11046
|
-
|
|
11046
|
+
/* Above the page content behind the seat (the seat is raised to 60 by
|
|
11047
|
+
the rule above while a parked menu is open), below the session-info
|
|
11048
|
+
sheet's 70 (info.css.ts). */
|
|
11047
11049
|
z-index: 61 !important;
|
|
11048
11050
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;
|
|
11049
11051
|
}
|
|
11050
|
-
|
|
11052
|
+
[data-zen-sheet-extras] [role="menu"] [role="menuitemradio"] {
|
|
11051
11053
|
min-height: 48px !important;
|
|
11052
11054
|
border-radius: 12px !important;
|
|
11053
11055
|
font-size: 15px !important;
|
|
@@ -11075,7 +11077,10 @@ exports.COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
|
|
|
11075
11077
|
body:has(${MODEL} > [class$="_menu"]) [data-chat-flow] + div,
|
|
11076
11078
|
body:has(> [id$="-menu"][class$="_menu"]:is([role="menu"], [role="group"])) [data-chat-flow] + div,
|
|
11077
11079
|
/* T70: the portaled permission menu (marked by effects/permission-sheet.ts). */
|
|
11078
|
-
body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div
|
|
11080
|
+
body:has(> div[role="menu"][data-zen-sheet="perm"]) [data-chat-flow] + div,
|
|
11081
|
+
/* T-issue30: an inline menu a parked chip opened (escaped to a fixed
|
|
11082
|
+
sheet by 4a). */
|
|
11083
|
+
body:has([data-zen-sheet-extras] [role="menu"]) [data-chat-flow] + div {
|
|
11079
11084
|
display: none !important;
|
|
11080
11085
|
}
|
|
11081
11086
|
/* --- 5. input box: two lines minimum, five lines maximum ---
|
|
@@ -14209,164 +14214,288 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14209
14214
|
exports.installModelSheetExtras = installModelSheetExtras;
|
|
14210
14215
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
14211
14216
|
const PHONE_QUERY = '(max-width: 767px)';
|
|
14212
|
-
/** The
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
* a bottom sheet — inline under the pill on ≤ 0.1.2, portaled to body on
|
|
14216
|
-
* 0.1.5+ (same two spellings as composer.css.ts's MODEL_MENU; the body form
|
|
14217
|
-
* is the only body-level `role=menu` with an `-menu` id, measured on
|
|
14218
|
-
* 0.1.5-rc.2). Appending into the portal root is no different from the
|
|
14219
|
-
* inline case: React leaves nodes it did not create alone, and the sheet
|
|
14220
|
-
* still unmounts on close, which is what the detached-menu path handles. */
|
|
14221
|
-
const MENU_SELECTOR = '[data-slot="conversation.input.model"] [class$="_menu"], body > [id$="-menu"][role="menu"]';
|
|
14217
|
+
/** The composer bar slot: the scan root, and the anchor the synthetic dock
|
|
14218
|
+
* rail hangs beside (hero stage). */
|
|
14219
|
+
const BAR_SELECTOR = '[data-slot="conversation.composer.bar"]';
|
|
14222
14220
|
/**
|
|
14223
|
-
*
|
|
14224
|
-
*
|
|
14225
|
-
*
|
|
14221
|
+
* Where a secondary control can legally sit in the composer, as ONE selector
|
|
14222
|
+
* list scanned inside the bar. Slots first (input.right / input.left), then
|
|
14223
|
+
* the official flex groups, then the row itself — a third party may inject
|
|
14224
|
+
* at any of the six (subscriptions and vision-router register into
|
|
14225
|
+
* input.right; dsh-workbuddy's credits and dsh-skills-ui's chips hang off
|
|
14226
|
+
* _row directly). Every position excludes `[data-mobile-nav]` so this
|
|
14227
|
+
* plugin's own nodes (the strip itself, and anything else we mount) are
|
|
14228
|
+
* never collected, and the official controls by name, so the whitelist
|
|
14229
|
+
* never fights the host. opt-in by marker, never `:not()`-hide unclaimed
|
|
14230
|
+
* nodes — the failure direction is "extra control stays visible", never
|
|
14231
|
+
* "control hidden from both places".
|
|
14226
14232
|
*/
|
|
14227
|
-
const EXTRAS_SELECTOR =
|
|
14233
|
+
const EXTRAS_SELECTOR = [
|
|
14234
|
+
'[data-slot="conversation.input.right"] > *:not([data-mobile-nav])',
|
|
14235
|
+
'[data-slot="conversation.input.left"] > *:not([data-mobile-nav])',
|
|
14236
|
+
'[class$="_tools"] > *:not(button[class$="_add"]):not(input[type="file"]):not([class$="_modes"]):not([data-slot="conversation.input.left"]):not([data-mobile-nav])',
|
|
14237
|
+
'[class$="_standardControls"] > *:not([data-slot="conversation.input.model"]):not([data-slot="conversation.input.right"]):not([data-mobile-nav])',
|
|
14238
|
+
'[class$="_trailing"] > *:not([class$="_standardControls"]):not([class$="_activity"]):not([class$="_primary"]):not([data-mobile-nav])',
|
|
14239
|
+
'[class$="_row"] > *:not([class$="_tools"]):not([class$="_trailing"]):not([data-mobile-nav])',
|
|
14240
|
+
].join(', ');
|
|
14228
14241
|
/**
|
|
14229
|
-
* dsh-vision-router's own mount marker. Used
|
|
14230
|
-
*
|
|
14242
|
+
* dsh-vision-router's own mount marker. Used only to answer "is the single
|
|
14243
|
+
* collected control the vision toggle" — see `worthMoving`.
|
|
14231
14244
|
*/
|
|
14232
14245
|
const VISION_SELECTOR = '[data-vision-router-mode-toggle]';
|
|
14233
14246
|
/**
|
|
14234
|
-
*
|
|
14235
|
-
*
|
|
14236
|
-
*
|
|
14237
|
-
*
|
|
14238
|
-
*
|
|
14239
|
-
*
|
|
14247
|
+
* On the input.right CONTAINER while the effect manages any of its children,
|
|
14248
|
+
* and on the strip itself. It is the styling hook for both halves in
|
|
14249
|
+
* composer.css.ts (the emptied container hides without leaving a flex gap;
|
|
14250
|
+
* an inline `[role="menu"]` opened by a parked chip escapes the strip's
|
|
14251
|
+
* overflow clip through it). It is deliberately what the row's
|
|
14252
|
+
* `display: none` keys off: without the marker the controls stay exactly
|
|
14253
|
+
* where the host put them, so a version of this file that declines to act —
|
|
14254
|
+
* or never runs at all — cannot leave them hidden in the row and absent from
|
|
14255
|
+
* the strip, i.e. unreachable.
|
|
14240
14256
|
*/
|
|
14241
14257
|
const MANAGED = 'data-zen-sheet-extras';
|
|
14242
14258
|
/**
|
|
14243
|
-
*
|
|
14259
|
+
* On every collected NODE while it is parked in the strip. Purely opt-in:
|
|
14260
|
+
* the marker's absence never hides anything, and `releaseAll` removes it —
|
|
14261
|
+
* an unmarked control is the host's own business again.
|
|
14262
|
+
*/
|
|
14263
|
+
const EXTRA = 'data-zen-composer-extra';
|
|
14264
|
+
/**
|
|
14265
|
+
* Is there anything collected that actually costs the row width?
|
|
14244
14266
|
*
|
|
14245
14267
|
* The row is one nowrap flex line whose only elastic item is the model pill
|
|
14246
14268
|
* (composer.css.ts section 1), so entries here are paid for out of the model
|
|
14247
|
-
* name. But that is only a problem once something WIDE lands
|
|
14248
|
-
*
|
|
14249
|
-
*
|
|
14250
|
-
* accommodates fine, and moving it on its own would cost
|
|
14251
|
-
*
|
|
14269
|
+
* name. But that is only a problem once something WIDE lands there — today
|
|
14270
|
+
* dsh-plugin-subscriptions' speed chip, which renders "速度 · 标准" and eats
|
|
14271
|
+
* ~70px. dsh-vision-router's toggle alone is a 28px icon that the row
|
|
14272
|
+
* accommodates fine, and moving it on its own would cost nothing visually
|
|
14273
|
+
* but would hide a one-tap control behind a scroll (user review, 2026-09-06).
|
|
14252
14274
|
*
|
|
14253
|
-
* So:
|
|
14254
|
-
* toggle. With no such entry — subscriptions not
|
|
14255
|
-
* not contributing on this model — nothing moves
|
|
14256
|
-
*
|
|
14275
|
+
* So: move only when the collection holds an entry that is not (or does not
|
|
14276
|
+
* contain) the vision toggle. With no such entry — subscriptions not
|
|
14277
|
+
* installed, or installed but not contributing on this model — nothing moves
|
|
14278
|
+
* and the row keeps its one icon. Why width rather than sniffing for
|
|
14279
|
+
* subscriptions by name: the slot system adds no per-registration marker, so
|
|
14280
|
+
* naming the plugin would anchor on a marker we'd have to add to our own
|
|
14281
|
+
* fork of it, and the feature would die the day the fork is dropped. The
|
|
14282
|
+
* width question is the real question anyway, and this asks it directly.
|
|
14257
14283
|
*
|
|
14258
|
-
*
|
|
14259
|
-
*
|
|
14260
|
-
*
|
|
14261
|
-
*
|
|
14262
|
-
* marker dies the day the fork is dropped, and the feature would go quiet
|
|
14263
|
-
* with nothing to catch it. The width question is the real question anyway,
|
|
14264
|
-
* and this asks it directly.
|
|
14284
|
+
* Unlike the pre-#30 design, NOTHING here depends on the official model
|
|
14285
|
+
* pill existing: replaced model slots (…codex-ui) and subagent sessions —
|
|
14286
|
+
* which have no pill at all — collect and relocate exactly like everyone
|
|
14287
|
+
* else.
|
|
14265
14288
|
*/
|
|
14266
|
-
function worthMoving(
|
|
14267
|
-
|
|
14289
|
+
function worthMoving(controls) {
|
|
14290
|
+
if (controls.length === 0)
|
|
14291
|
+
return false;
|
|
14292
|
+
if (controls.length === 1) {
|
|
14293
|
+
const child = controls[0];
|
|
14294
|
+
if (child === undefined)
|
|
14295
|
+
return false;
|
|
14296
|
+
// "is or contains" — some plugins wrap their toggle in a positioning node.
|
|
14297
|
+
if (child.matches(VISION_SELECTOR) || child.querySelector(VISION_SELECTOR) !== null)
|
|
14298
|
+
return false;
|
|
14299
|
+
}
|
|
14300
|
+
return true;
|
|
14268
14301
|
}
|
|
14269
14302
|
/**
|
|
14270
|
-
* Phone:
|
|
14271
|
-
*
|
|
14272
|
-
*
|
|
14273
|
-
* row runs out of width. Both are model-scoped settings, so the model sheet —
|
|
14274
|
-
* which already holds 模型 and 推理等级 — is where they belong (user request,
|
|
14275
|
-
* 2026-09-06).
|
|
14303
|
+
* Phone: move the composer row's secondary third-party controls into a
|
|
14304
|
+
* 26px horizontally-scrollable strip inside the dock rail above the card —
|
|
14305
|
+
* always visible, one tap from anywhere, no sheet in between.
|
|
14276
14306
|
*
|
|
14277
|
-
* Why move the real controls instead of drawing our own rows that drive
|
|
14278
|
-
* the same reason native-trigger-overlay.ts moves the real trigger. A
|
|
14279
|
-
* has to script the original, which is a road this plugin does not
|
|
14280
|
-
* beyond the trusted-input wall that effect documents, our own
|
|
14281
|
-
* to mirror every piece of state (speed tier, disabled,
|
|
14282
|
-
* a vision twin exists for the current model) and
|
|
14283
|
-
* upstream restyle. Moving the node keeps one source
|
|
14284
|
-
* roots go on owning and updating these elements, we
|
|
14285
|
-
* hang
|
|
14307
|
+
* Why move the real controls instead of drawing our own rows that drive
|
|
14308
|
+
* them: the same reason native-trigger-overlay.ts moves the real trigger. A
|
|
14309
|
+
* stand-in has to script the original, which is a road this plugin does not
|
|
14310
|
+
* go down — beyond the trusted-input wall that effect documents, our own
|
|
14311
|
+
* rows would have to mirror every piece of state (speed tier, disabled,
|
|
14312
|
+
* aria-pressed, whether a vision twin exists for the current model) and
|
|
14313
|
+
* would rot on the next upstream restyle. Moving the node keeps one source
|
|
14314
|
+
* of truth: their React roots go on owning and updating these elements, we
|
|
14315
|
+
* only change where they hang — never cloneNode, and the move is anchored
|
|
14316
|
+
* with a Comment at the original spot so `unparkAll` can put each node back
|
|
14317
|
+
* exactly where React left it.
|
|
14286
14318
|
*
|
|
14287
|
-
* Measured 2026-09-06 (DSH 0.1.2, 390px) before
|
|
14288
|
-
* that would have killed the approach both
|
|
14289
|
-
* host re-renders (typing into the
|
|
14290
|
-
* yank it back) and stays
|
|
14319
|
+
* Measured 2026-09-06 (DSH 0.1.2, 390px) before the original version of
|
|
14320
|
+
* this effect — the two things that would have killed the approach both
|
|
14321
|
+
* hold: a relocated control survives host re-renders (typing into the
|
|
14322
|
+
* composer re-renders the row and does not yank it back) and stays
|
|
14323
|
+
* hit-testable at its new position.
|
|
14291
14324
|
*
|
|
14292
|
-
*
|
|
14293
|
-
*
|
|
14294
|
-
*
|
|
14295
|
-
*
|
|
14296
|
-
*
|
|
14297
|
-
*
|
|
14325
|
+
* Known hazard, inherent to relocating React-owned nodes: a control whose
|
|
14326
|
+
* REGISTRATION unmounts while parked (plugin disabled, slot re-keyed) is
|
|
14327
|
+
* removed by React through its ORIGINAL parent — `parent.removeChild(child)`
|
|
14328
|
+
* throws when the child moved, and the exception lands in React's commit.
|
|
14329
|
+
* Updates that keep the registration mounted (state-driven re-renders,
|
|
14330
|
+
* text/attribute changes, a control's own inner nodes unmounting) are safe:
|
|
14331
|
+
* those removals run inside the parked subtree, where every parent-child
|
|
14332
|
+
* link React recorded still holds. Parking is also fully reversible — any
|
|
14333
|
+
* decline path (desktop, nothing worth moving, teardown) hands every node
|
|
14334
|
+
* back before anything is removed.
|
|
14298
14335
|
*/
|
|
14299
14336
|
function installModelSheetExtras(ctx) {
|
|
14300
14337
|
ctx.effect(() => {
|
|
14301
14338
|
const phone = window.matchMedia(PHONE_QUERY);
|
|
14302
|
-
/**
|
|
14303
|
-
let parked =
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14339
|
+
/** Parked nodes with the Comment anchoring each one's original spot. */
|
|
14340
|
+
let parked = [];
|
|
14341
|
+
let syntheticDock = null;
|
|
14342
|
+
/** Collected controls, document order; parked ones appended so the set
|
|
14343
|
+
* stays stable while they live in the strip. */
|
|
14344
|
+
const collect = (bar) => {
|
|
14345
|
+
const controls = Array.from(bar.querySelectorAll(EXTRAS_SELECTOR));
|
|
14346
|
+
for (const { node } of parked) {
|
|
14347
|
+
if (node.isConnected && !controls.includes(node))
|
|
14348
|
+
controls.push(node);
|
|
14349
|
+
}
|
|
14350
|
+
return controls;
|
|
14309
14351
|
};
|
|
14310
|
-
const
|
|
14311
|
-
if (
|
|
14352
|
+
const park = (node, strip) => {
|
|
14353
|
+
if (node.parentElement === null)
|
|
14312
14354
|
return;
|
|
14313
|
-
const
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
// owning React roots rebuild the slot on the next mount).
|
|
14319
|
-
if (parent.isConnected)
|
|
14320
|
-
parent.insertBefore(node, next);
|
|
14355
|
+
const placeholder = document.createComment('zen-composer-extra');
|
|
14356
|
+
node.parentElement.insertBefore(placeholder, node);
|
|
14357
|
+
node.setAttribute(EXTRA, 'open');
|
|
14358
|
+
strip.appendChild(node);
|
|
14359
|
+
parked.push({ node, placeholder });
|
|
14321
14360
|
};
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14361
|
+
const unparkAll = () => {
|
|
14362
|
+
for (const { node, placeholder } of parked) {
|
|
14363
|
+
// Re-home only while the anchor still marks the spot AND that home
|
|
14364
|
+
// is on the page — when the whole composer went away, the owning
|
|
14365
|
+
// React roots rebuild the slot on the next mount.
|
|
14366
|
+
if (placeholder.parentNode !== null && placeholder.parentNode.isConnected) {
|
|
14367
|
+
placeholder.parentNode.insertBefore(node, placeholder);
|
|
14368
|
+
}
|
|
14369
|
+
placeholder.remove();
|
|
14370
|
+
node.removeAttribute(EXTRA);
|
|
14371
|
+
}
|
|
14372
|
+
parked = [];
|
|
14373
|
+
};
|
|
14374
|
+
/** Everything the effect owns, back to host-placed defaults: parked
|
|
14375
|
+
* nodes re-homed, strip, synthetic rail and every marker gone.
|
|
14376
|
+
* Idempotent — teardown and every decline path go through here. */
|
|
14377
|
+
const releaseAll = () => {
|
|
14378
|
+
unparkAll();
|
|
14379
|
+
document.querySelector('[data-mobile-nav="composer-chips"]')?.remove();
|
|
14380
|
+
syntheticDock?.remove();
|
|
14381
|
+
syntheticDock = null;
|
|
14382
|
+
const bar = document.querySelector(BAR_SELECTOR);
|
|
14383
|
+
const scope = bar ?? document;
|
|
14384
|
+
for (const node of scope.querySelectorAll(`[${EXTRA}]`))
|
|
14385
|
+
node.removeAttribute(EXTRA);
|
|
14386
|
+
bar?.querySelector('[data-slot="conversation.input.right"]')?.removeAttribute(MANAGED);
|
|
14387
|
+
};
|
|
14388
|
+
/** The rail the strip lives in — the host's own dock, or a synthetic
|
|
14389
|
+
* stand-in while the hero composer has rendered none yet. */
|
|
14390
|
+
const adoptDock = (bar) => {
|
|
14391
|
+
// :not([data-zen-synthetic-dock]) is load-bearing: the synthetic rail
|
|
14392
|
+
// is bar's SIBLING carrying the same data-slot, so an unqualified
|
|
14393
|
+
// query would find it as the "real" dock, delete it as stale, recreate
|
|
14394
|
+
// it after — the observer would oscillate forever.
|
|
14395
|
+
const dock = bar.parentElement?.querySelector('[data-slot="conversation.input.dock"]:not([data-zen-synthetic-dock])') ?? null;
|
|
14396
|
+
if (dock !== null) {
|
|
14397
|
+
if (syntheticDock !== null) {
|
|
14398
|
+
// Real dock appeared: hand every parked node back before the
|
|
14399
|
+
// synthetic rail (and the strip inside it) is destroyed — removing
|
|
14400
|
+
// a rail full of React-owned controls would detach them from the
|
|
14401
|
+
// page. sync re-parks them into the new strip right after.
|
|
14402
|
+
unparkAll();
|
|
14403
|
+
syntheticDock.remove();
|
|
14404
|
+
syntheticDock = null;
|
|
14405
|
+
}
|
|
14406
|
+
return dock;
|
|
14407
|
+
}
|
|
14408
|
+
if (syntheticDock === null || !syntheticDock.isConnected) {
|
|
14409
|
+
syntheticDock = document.createElement('div');
|
|
14410
|
+
syntheticDock.setAttribute('data-slot', 'conversation.input.dock');
|
|
14411
|
+
syntheticDock.setAttribute('data-zen-synthetic-dock', '');
|
|
14412
|
+
bar.parentNode?.insertBefore(syntheticDock, bar);
|
|
14413
|
+
}
|
|
14414
|
+
return syntheticDock;
|
|
14415
|
+
};
|
|
14416
|
+
const ensureStrip = (home) => {
|
|
14417
|
+
let strip = home.querySelector('[data-mobile-nav="composer-chips"]');
|
|
14418
|
+
if (strip === null) {
|
|
14419
|
+
strip = document.createElement('div');
|
|
14420
|
+
strip.setAttribute('data-mobile-nav', 'composer-chips');
|
|
14421
|
+
strip.setAttribute(MANAGED, '');
|
|
14422
|
+
// Document-order first; CSS `order: 0` keeps it the rail's leftmost
|
|
14423
|
+
// pill line among the order-0 ties.
|
|
14424
|
+
home.prepend(strip);
|
|
14425
|
+
}
|
|
14426
|
+
return strip;
|
|
14326
14427
|
};
|
|
14327
14428
|
const sync = () => {
|
|
14328
|
-
const
|
|
14329
|
-
if (
|
|
14330
|
-
|
|
14429
|
+
const bar = document.querySelector(BAR_SELECTOR);
|
|
14430
|
+
if (bar === null) {
|
|
14431
|
+
releaseAll();
|
|
14331
14432
|
return;
|
|
14332
14433
|
}
|
|
14333
|
-
//
|
|
14334
|
-
//
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14434
|
+
// A parked node its own React root unmounted is dead weight; drop it
|
|
14435
|
+
// and its anchor before collecting, so it neither blocks nor hides.
|
|
14436
|
+
parked = parked.filter(({ node, placeholder }) => {
|
|
14437
|
+
if (node.isConnected)
|
|
14438
|
+
return true;
|
|
14439
|
+
placeholder.remove();
|
|
14440
|
+
return false;
|
|
14441
|
+
});
|
|
14442
|
+
const controls = collect(bar);
|
|
14443
|
+
if (!phone.matches || !worthMoving(controls)) {
|
|
14444
|
+
releaseAll();
|
|
14340
14445
|
return;
|
|
14341
14446
|
}
|
|
14342
|
-
|
|
14343
|
-
const
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14447
|
+
const home = adoptDock(bar);
|
|
14448
|
+
const strip = ensureStrip(home);
|
|
14449
|
+
// Parked entries the set has outgrown (an uninstalled plugin, a
|
|
14450
|
+
// re-render that moved a control into a claimed position) go home and
|
|
14451
|
+
// lose their marker.
|
|
14452
|
+
parked = parked.filter(({ node, placeholder }) => {
|
|
14453
|
+
if (controls.includes(node) && node.isConnected)
|
|
14454
|
+
return true;
|
|
14455
|
+
if (node.isConnected && placeholder.parentNode !== null && placeholder.parentNode.isConnected) {
|
|
14456
|
+
placeholder.parentNode.insertBefore(node, placeholder);
|
|
14457
|
+
}
|
|
14458
|
+
placeholder.remove();
|
|
14459
|
+
node.removeAttribute(EXTRA);
|
|
14460
|
+
return false;
|
|
14461
|
+
});
|
|
14462
|
+
for (const node of controls) {
|
|
14463
|
+
if (!parked.some((entry) => entry.node === node))
|
|
14464
|
+
park(node, strip);
|
|
14465
|
+
}
|
|
14466
|
+
// Stale markers anywhere in the bar — parked ones are in `controls`,
|
|
14467
|
+
// so they survive this sweep.
|
|
14468
|
+
for (const node of bar.querySelectorAll(`[${EXTRA}]`)) {
|
|
14469
|
+
if (!controls.includes(node))
|
|
14470
|
+
node.removeAttribute(EXTRA);
|
|
14471
|
+
}
|
|
14472
|
+
const right = bar.querySelector('[data-slot="conversation.input.right"]');
|
|
14473
|
+
if (right !== null) {
|
|
14474
|
+
const rightManaged = right.children.length > 0 ||
|
|
14475
|
+
parked.some(({ placeholder }) => placeholder.parentNode === right);
|
|
14476
|
+
if (rightManaged)
|
|
14477
|
+
right.setAttribute(MANAGED, '');
|
|
14478
|
+
else
|
|
14479
|
+
right.removeAttribute(MANAGED);
|
|
14480
|
+
}
|
|
14348
14481
|
};
|
|
14349
14482
|
// Synchronous in the observer callback, never behind rAF: the callback is
|
|
14350
|
-
// a microtask and runs before this frame paints, so the
|
|
14351
|
-
//
|
|
14352
|
-
//
|
|
14353
|
-
//
|
|
14354
|
-
//
|
|
14483
|
+
// a microtask and runs before this frame paints, so the strip and the
|
|
14484
|
+
// parked nodes are in place the first time their frame is drawn. A rAF
|
|
14485
|
+
// would land a frame late (AGENTS.md's "晚一帧" rule). Body-level
|
|
14486
|
+
// childList is the same shape modal-back.ts uses. The observer also sees
|
|
14487
|
+
// this effect's own moves; sync is idempotent, so the extra pass is a
|
|
14488
|
+
// no-op.
|
|
14355
14489
|
const observer = new MutationObserver(sync);
|
|
14356
|
-
observer.observe(document.body, {
|
|
14357
|
-
childList: true,
|
|
14358
|
-
subtree: true,
|
|
14359
|
-
attributes: true,
|
|
14360
|
-
attributeFilter: ['aria-expanded'],
|
|
14361
|
-
});
|
|
14490
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
14362
14491
|
phone.addEventListener('change', sync);
|
|
14363
14492
|
sync();
|
|
14364
14493
|
return () => {
|
|
14365
14494
|
observer.disconnect();
|
|
14366
14495
|
phone.removeEventListener('change', sync);
|
|
14367
|
-
|
|
14496
|
+
releaseAll();
|
|
14368
14497
|
};
|
|
14369
|
-
}, 'dsh-mobile-nav:
|
|
14498
|
+
}, 'dsh-mobile-nav: composer chips extras');
|
|
14370
14499
|
}
|
|
14371
14500
|
};
|
|
14372
14501
|
__modules["client/effects/permission-sheet.js"] = function (require, module, exports) {
|